Changes for page InplaceEditing
Last modified by SuperNico Laub on 2025/09/18 17:55
From version 2.1
edited by SuperNico Laub
on 2025/09/18 17:55
on 2025/09/18 17:55
Change comment:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/17.7.0]
To version 1.1
edited by superadmin
on 2025/05/22 17:45
on 2025/05/22 17:45
Change comment:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/16.10.8]
Summary
-
Page properties (1 modified, 0 added, 0 removed)
-
Objects (3 modified, 0 added, 0 removed)
Details
- Page properties
-
- Author
-
... ... @@ -1,1 +1,1 @@ 1 - xwiki:XWiki.supernico1 +XWiki.superadmin
- XWiki.JavaScriptExtension[0]
-
- Code
-
... ... @@ -92,12 +92,12 @@ 92 92 /** 93 93 * Render the title and the content of this document. 94 94 * 95 - * @param {object}params- optionalrenderingparameters,thatmayinclude theoutputsyntax and the rendering96 - * transformations to execute 95 + * @param forView whether to render the document for view (without the rendering annotations) or for edit (with the 96 + * rendering annotations); when rendering for edit some transformations might not be executed 97 97 * @return a promise that resolves to this document instance if the render request succeeds 98 98 */ 99 - render( {outputSyntax, transformations} = {}) {100 - constqueryString = {99 + render(forView) { 100 + var queryString = { 101 101 xpage: 'get', 102 102 outputTitle: true, 103 103 // Render the default translation if this is a new document: ... ... @@ -108,21 +108,21 @@ 108 108 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 109 109 timestamp: new Date().getTime() 110 110 }; 111 - if (outputSyntax) { 112 - queryString.outputSyntax = outputSyntax.type; 113 - queryString.outputSyntaxVersion = outputSyntax.version; 111 + if (!forView) { 112 + // We need the annotated HTML when editing in order to be able to protect the rendering transformations and to 113 + // be able to recreate the wiki syntax. 114 + queryString.outputSyntax = 'annotatedhtml'; 115 + queryString.outputSyntaxVersion = '5.0' 116 + // Currently, only the macro transformations are protected and thus can be edited. 117 + // See XRENDERING-78: Add markers to modified XDOM by Transformations/Macros 118 + queryString.transformations = 'macro'; 114 114 } 115 - if (transformations) { 116 - queryString.transformations = transformations; 117 - } 118 - return Promise.resolve($.get(this.getURL('view'), $.param(queryString, true))).then(html => { 120 + return Promise.resolve($.get(this.getURL('view'), queryString)).then(html => { 119 119 // Render succeeded. 120 - const container = $('<div/>').html(html); 121 - const contentWrapper = container.find('#xwikicontent'); 122 - const isContentRenderedToHTML = !outputSyntax || outputSyntax.type.includes('html'); 122 + var container = $('<div/>').html(html); 123 123 return $.extend(this, { 124 124 renderedTitle: container.find('#document-title h1').html(), 125 - renderedContent: isContentRenderedToHTML ?contentWrapper.html(): contentWrapper.text()125 + renderedContent: container.find('#xwikicontent').html() 126 126 }); 127 127 }).catch(() => { 128 128 new XWiki.widgets.Notification(l10n['edit.inplace.page.renderFailed'], 'error'); ... ... @@ -195,6 +195,7 @@ 195 195 lockAction: action, 196 196 force: force, 197 197 language: this.getRealLocale(), 198 + outputSyntax: 'plain', 198 198 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 199 199 timestamp: new Date().getTime() 200 200 })).then(() => { ... ... @@ -484,18 +484,18 @@ 484 484 }).catch(xwikiDocument => { 485 485 new XWiki.widgets.Notification(l10n['edit.inplace.page.loadFailed'], 'error'); 486 486 return Promise.reject(xwikiDocument); 487 - // Render the document for edit, usingthe providedrenderingconfiguration,which maydepend onthe editor used to488 - // edit the content. 489 - }).then(render.bind(null, module.contentEditor.getRenderingConfig()));488 + // Render the document for edit, in order to have the annotated content HTML. The annotations are used to protect 489 + // the rendering transformations (e.g. macros) when editing the content. 490 + }).then(render.bind(null, false)); 490 490 }; 491 491 492 492 /** 493 - * @param {object}params- optionalrenderingparameters,thatmayinclude theoutputsyntax and the rendering494 - * transformations to execute 494 + * @param forView whether to render the document for view (without the rendering annotations) or for edit (with the 495 + * rendering annotations); when rendering for edit some transformations might not be executed 495 495 * @param xwikiDocument the document to render 496 496 */ 497 - var render = function( params, xwikiDocument) {498 - return xwikiDocument.render( params);498 + var render = function(forView, xwikiDocument) { 499 + return xwikiDocument.render(forView); 499 499 }; 500 500 501 501 var maybeSave = function(xwikiDocument) { ... ... @@ -584,7 +584,7 @@ 584 584 585 585 // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML 586 586 // both if we stop editing now and if we continue but cancel the edit later. 587 - return xwikiDocument.reload().then(render.bind(null, {})).then(588 + return xwikiDocument.reload().then(render.bind(null, true)).then( 588 588 afterReloadAndRender.bind(null, /* success: */ true), 589 589 afterReloadAndRender.bind(null, /* success: */ false) 590 590 ); ... ... @@ -633,7 +633,7 @@ 633 633 return view(xwikiDocument, true).then(editInPlace); 634 634 }; 635 635 636 - a syncfunctionview(xwikiDocument, reload) {637 + var view = function(xwikiDocument, reload) { 637 637 if (xwikiDocument.isNew && xwikiDocument.language && xwikiDocument.defaultTranslation) { 638 638 // The user tried to translate the current document in the UI locale and either canceled the edit without saving 639 639 // or there was an error. Display the default translation to the user. ... ... @@ -640,51 +640,29 @@ 640 640 xwikiDocument = xwikiDocument.defaultTranslation; 641 641 } 642 642 643 - const viewContent = $('#xwikicontent'); 644 - // Destroy the editors and restore the view mode. 645 - const promises = []; 646 - viewContent.trigger('xwiki:actions:view', {document: xwikiDocument, promises}); 647 - // Some listeners may need to perform asynchronous operations. We wait for them, but we restore the view mode even 648 - // if they fail. 649 - logRejectedPromises( 650 - 'We encountered some errors while restoring the view mode: ', 651 - await Promise.allSettled(promises) 652 - ); 644 + var viewContent = $('#xwikicontent'); 645 + // Destroy the editors before returning to view. 646 + viewContent.trigger('xwiki:actions:view', {document: xwikiDocument}); 653 653 $('#document-title h1').html(xwikiDocument.renderedTitle); 654 654 viewContent.html(xwikiDocument.renderedContent); 655 - 656 656 // Reset the editor to let others know that we're not editing anymore. 657 657 XWiki.editor = ''; 658 - 659 659 if (!reload) { 660 660 // If the user has canceled the edit then the restored page content may include the section edit links. Show them 661 661 // in case they were hidden. 662 662 viewContent.children(':header').children('.edit_section').removeClass('hidden'); 663 - 664 664 // Let others know that the DOM has been updated, in order to enhance it. 665 665 $(document).trigger('xwiki:dom:updated', {'elements': viewContent.toArray()}); 666 666 } 667 - 668 668 // Remove the action events scope. 669 669 viewContent.closest('.form').removeClass('form'); 670 - 671 671 // Update the URL. 672 672 if (window.location.hash === '#edit' || window.location.hash === '#translate') { 673 673 history.replaceState(null, null, '#'); 674 674 } 664 + return Promise.resolve(xwikiDocument); 665 + }; 675 675 676 - return xwikiDocument; 677 - } 678 - 679 - function logRejectedPromises(message, results) { 680 - const errors = results 681 - .filter(result => result.status === 'rejected') 682 - .map(result => result.reason); 683 - if (errors.length) { 684 - console.warn(message, errors); 685 - } 686 - } 687 - 688 688 var edit = function(xwikiDocument) { 689 689 // By adding the 'form' CSS class we set the scope of the action events (e.g. xwiki:actions:beforeSave or 690 690 // xwiki:actions:cancel). We need this because in view mode we can have multiple forms active on the page (e.g. one ... ... @@ -713,6 +713,9 @@ 713 713 <div hidden> 714 714 <input type="hidden" name="form_token" /> 715 715 <input type="hidden" name="async" value="true" /> 695 + <input type="hidden" name="content" /> 696 + <input type="hidden" name="RequiresHTMLConversion" value="content" /> 697 + <input type="hidden" name="content_syntax" /> 716 716 <input type="hidden" name="language" /> 717 717 </div> 718 718 <fieldset id="xwikieditcontent" class="xform inplace-editing-buttons sticky-buttons"></fieldset> ... ... @@ -833,6 +833,16 @@ 833 833 form.find('input[name="language"]').val(xwikiDocument.getRealLocale()); 834 834 form.find('input[name="isNew"]').val(xwikiDocument.isNew); 835 835 818 + // Submit either the raw (source) content (no syntax conversion needed in this case) or the rendered content (HTML) 819 + // in which case we have to force the conversion to the document syntax on the server. 820 + const submitRawContent = typeof xwikiDocument.renderedContent !== 'string'; 821 + form.find('input[name="content"]').val(submitRawContent ? xwikiDocument.content : xwikiDocument.renderedContent); 822 + form.find('input[name="RequiresHTMLConversion"]').prop('disabled', submitRawContent); 823 + form.find('input[name="content_syntax"]').val(xwikiDocument.syntax).prop('disabled', submitRawContent); 824 + 825 + // Add the temporary uploaded files to the form. 826 + $('#xwikicontent').nextAll('input[name="uploadedFiles"]').attr('form', 'inplace-editing'); 827 + 836 836 // Check for merge conflicts only if the document is not new and we know the current version. 837 837 if (!xwikiDocument.isNew && xwikiDocument.version) { 838 838 form.find('input[name="previousVersion"]').val(xwikiDocument.version); ... ... @@ -844,13 +844,14 @@ 844 844 var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype); 845 845 $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, { 846 846 reloadEditor: function() { 847 - if (XWiki.editor === config.editMode) { 839 + var actionButtons = $('.inplace-editing-buttons'); 840 + if (actionButtons.is(':visible')) { 848 848 // This function is called after the document save confirmation is received, if the save was done by merge. We 849 849 // register our reload listener from a document saved listener, but we're using promises which are 850 850 // asynchronous so the reload listener is actually registered with a delay. For this reason we trigger the 851 851 // reload event with a delay to ensure our reload listener is called. 852 852 setTimeout(function() { 853 - $('.inplace-editing-buttons').trigger('xwiki:actions:reload');846 + actionButtons.trigger('xwiki:actions:reload'); 854 854 }, 0); 855 855 } else { 856 856 return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments); ... ... @@ -857,13 +857,13 @@ 857 857 } 858 858 }, 859 859 maybeRedirect: function(continueEditing) { 860 - if ( XWiki.editor === config.editMode) {853 + if ($('.inplace-editing-buttons').is(':visible')) { 861 861 // Overwrite the default behavior so that we don't redirect when leaving the edit mode because we're already 862 862 // in view mode. We still need to report a redirect (return true) if we don't continue editing, so that 863 863 // actionButtons.js behaves as if a redirect was done. 864 864 return !continueEditing; 865 865 } else { 866 - // Fallback on the default behavior if we'renoteditingin-place.859 + // Fallback on the default behavior if the in-place editing buttons are hidden. 867 867 return originalAjaxSaveAndContinue.maybeRedirect.apply(this, arguments); 868 868 } 869 869 } ... ... @@ -906,7 +906,9 @@ 906 906 var withFocus = document.activeElement && document.activeElement === editContent[0]; 907 907 // Keep showing the view content until the edit content is ready in order to avoid UI flicker. 908 908 var viewContent = editContent.clone().insertAfter(editContent); 909 - editContent.hide().empty(); 902 + // Note that we don't trigger the xwiki:dom:updated event here because we want to let the editor trigger the event 903 + // only for the content areas that are safe to be updated from JavaScript (i.e. the macro output). 904 + editContent.hide().html(xwikiDocument.renderedContent); 910 910 if (withFocus) { 911 911 // Keep the focus while the edit content is being prepared. 912 912 viewContent.focus(); ... ... @@ -915,8 +915,6 @@ 915 915 // Use the same name as for the standalone editor, in order to be consistent. 916 916 editorName: 'content', 917 917 document: xwikiDocument, 918 - startupFocus: withFocus, 919 - formId: 'inplace-editing', 920 920 // The content editor is loaded on demand, asynchronously. 921 921 deferred: $.Deferred() 922 922 }); ... ... @@ -924,6 +924,14 @@ 924 924 return data.deferred.promise().then(() => { 925 925 editContent.show(); 926 926 viewContent.remove(); 920 + if (withFocus) { 921 + // Restore the focus when the edit content is ready but make sure we don't scroll the page. We don't restore the 922 + // focus right away because we just made the content visible so it may not be editable yet (e.g. the WYSIWYG 923 + // editor can make the content editable only if it is visible). 924 + setTimeout(function() { 925 + editContent[0].focus({preventScroll: true}); 926 + }, 0); 927 + } 927 927 return xwikiDocument; 928 928 }); 929 929 }; ... ... @@ -938,15 +938,12 @@ 938 938 } 939 939 }; 940 940 941 - let module = { 942 - contentEditor: undefined, 942 + return { 943 943 preload, 944 944 editPage, 945 945 editSection, 946 946 translatePage 947 947 }; 948 - 949 - return module; 950 950 }); 951 951 952 952 require(['jquery'], function($) { ... ... @@ -955,15 +955,16 @@ 955 955 return; 956 956 } 957 957 956 + var wysiwygEditorModule = 'xwiki-' + config.wysiwygEditor + '-inline'; 957 + 958 958 var preloadEditor = function() { 959 - require(['editInPlace', config.contentEditor], function(editInPlace, contentEditor) { 960 - editInPlace.contentEditor = contentEditor; 959 + require(['editInPlace', wysiwygEditorModule], function(editInPlace) { 961 961 editInPlace.preload(); 962 962 // Fallback on the standalone edit mode if we fail to load the required modules. 963 963 }, disableInPlaceEditing); 964 964 }; 965 965 966 - // Preload the contenteditor code without slowing down the page view.965 + // Preload the WYSIWYG editor code without slowing down the page view. 967 967 if (document.readyState === 'complete') { 968 968 setTimeout(preloadEditor, 0); 969 969 } else { ... ... @@ -1017,8 +1017,7 @@ 1017 1017 const data = handler.beforeEdit?.(event); 1018 1018 // Load the code needed to edit in place only when the edit button is clicked. 1019 1019 currentlyEditing = new Promise((resolve, reject) => { 1020 - require(['editInPlace', config.contentEditor], (editInPlace, contentEditor) => { 1021 - editInPlace.contentEditor = contentEditor; 1019 + require(['editInPlace', wysiwygEditorModule], (editInPlace) => { 1022 1022 // Re-enable the translate button because it can be used while editing to create the missing translation. 1023 1023 translateButton.removeClass('disabled'); 1024 1024 handler.edit(editInPlace, data).finally(() => {
- XWiki.StyleSheetExtension[0]
-
- Code
-
... ... @@ -1,3 +1,29 @@ 1 +#document-title h1.editable { 2 + /* Move the title heading a bit to the top and to the left in order to accomodate the input border and padding. */ 3 + margin-top: -@line-height-computed / 4; 4 + margin-left: -(ceil(@grid-gutter-width / 2)); 5 + /* Reduce the bottom margin in order to accomodate the input border and bottom padding. */ 6 + margin-bottom: @line-height-computed / 4; 7 +} 8 + 9 +@document-title-input-padding-vertical: @line-height-computed / 4 - 1; 10 +.document-header input#document-title-input { 11 + /* Preserve the heading styles. */ 12 + color: inherit; 13 + font-size: inherit; 14 + background-color: transparent; 15 + /* It seems it's not enough to set the line height for the text input. We also need to set its height. */ 16 + height: ~"calc(@{font-size-document-title} * @{headings-line-height} - -2 * (1px - -@{document-title-input-padding-vertical}))"; 17 + line-height: @headings-line-height; 18 + padding: @document-title-input-padding-vertical (ceil(@grid-gutter-width / 2) - 1); 19 + width: 100%; 20 +} 21 +input#document-title-input:valid { 22 + border: 1px solid transparent; 23 + box-shadow: none; 24 +} 25 + 26 +input#document-title-input:valid:focus, 1 1 #xwikicontent[contenteditable]:focus, 2 2 #xwikicontent[tabindex]:focus { 3 3 outline: 0;
- XWiki.UIExtensionClass[0]
-
- Executed Content
-
... ... @@ -39,12 +39,16 @@ 39 39 'colorTheme': $services.model.serialize($themeDoc.documentReference, 'default') 40 40 }) 41 41 #set ($jsParams = {'language': $xcontext.locale}) 42 + ## We have to explicitly enable the source mode for in-line edit because the latest version of the content editor 43 + ## could be installed on an older version of XWiki where the in-place editor didn't support the source mode (so the 44 + ## content editor cannot enable the source mode by default). 42 42 #set ($inplaceEditingConfig = { 43 43 'contentType': 'org.xwiki.rendering.syntax.SyntaxContent', 44 44 'editMode': $defaultEditMode, 45 - ' contentEditor':"xwiki-${services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id}-inline",48 + 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id, 46 46 'editButtonSelector': '#tmEdit > a', 47 47 'translateButtonSelector': '#tmTranslate > a', 51 + 'enableSourceMode': true, 48 48 'paths': { 49 49 'js': { 50 50 'xwiki-actionButtons': "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.js' $jsParams)",