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
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
Change comment: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/16.10.8]

Summary

Details

Page properties
Author
... ... @@ -1,1 +1,1 @@
1 -xwiki:XWiki.supernico
1 +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 - optional rendering parameters, that may include the output syntax and the rendering
96 - * 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 - const queryString = {
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, using the provided rendering configuration, which may depend on the editor used to
488 - // 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 - optional rendering parameters, that may include the output syntax and the rendering
494 - * 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 - async function view(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're not editing in-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 content editor 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)",