Changes for page InplaceEditing

Last modified by SuperNico Laub on 2025/09/18 17:55

From 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]
To 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]

Summary

Details

Page properties
Author
... ... @@ -1,1 +1,1 @@
1 -XWiki.superadmin
1 +xwiki:XWiki.supernico
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 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
95 + * @param {object} params - optional rendering parameters, that may include the output syntax and the rendering
96 + * transformations to execute
97 97   * @return a promise that resolves to this document instance if the render request succeeds
98 98   */
99 - render(forView) {
100 - var queryString = {
99 + render({outputSyntax, transformations} = {}) {
100 + const 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 (!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';
111 + if (outputSyntax) {
112 + queryString.outputSyntax = outputSyntax.type;
113 + queryString.outputSyntaxVersion = outputSyntax.version;
119 119   }
120 - return Promise.resolve($.get(this.getURL('view'), queryString)).then(html => {
115 + if (transformations) {
116 + queryString.transformations = transformations;
117 + }
118 + return Promise.resolve($.get(this.getURL('view'), $.param(queryString, true))).then(html => {
121 121   // Render succeeded.
122 - var container = $('<div/>').html(html);
120 + const container = $('<div/>').html(html);
121 + const contentWrapper = container.find('#xwikicontent');
122 + const isContentRenderedToHTML = !outputSyntax || outputSyntax.type.includes('html');
123 123   return $.extend(this, {
124 124   renderedTitle: container.find('#document-title h1').html(),
125 - renderedContent: container.find('#xwikicontent').html()
125 + renderedContent: isContentRenderedToHTML ? contentWrapper.html() : contentWrapper.text()
126 126   });
127 127   }).catch(() => {
128 128   new XWiki.widgets.Notification(l10n['edit.inplace.page.renderFailed'], 'error');
... ... @@ -195,7 +195,6 @@
195 195   lockAction: action,
196 196   force: force,
197 197   language: this.getRealLocale(),
198 - outputSyntax: 'plain',
199 199   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
200 200   timestamp: new Date().getTime()
201 201   })).then(() => {
... ... @@ -485,18 +485,18 @@
485 485   }).catch(xwikiDocument => {
486 486   new XWiki.widgets.Notification(l10n['edit.inplace.page.loadFailed'], 'error');
487 487   return Promise.reject(xwikiDocument);
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));
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()));
491 491   };
492 492  
493 493   /**
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
493 + * @param {object} params - optional rendering parameters, that may include the output syntax and the rendering
494 + * transformations to execute
496 496   * @param xwikiDocument the document to render
497 497   */
498 - var render = function(forView, xwikiDocument) {
499 - return xwikiDocument.render(forView);
497 + var render = function(params, xwikiDocument) {
498 + return xwikiDocument.render(params);
500 500   };
501 501  
502 502   var maybeSave = function(xwikiDocument) {
... ... @@ -585,7 +585,7 @@
585 585  
586 586   // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML
587 587   // both if we stop editing now and if we continue but cancel the edit later.
588 - return xwikiDocument.reload().then(render.bind(null, true)).then(
587 + return xwikiDocument.reload().then(render.bind(null, {})).then(
589 589   afterReloadAndRender.bind(null, /* success: */ true),
590 590   afterReloadAndRender.bind(null, /* success: */ false)
591 591   );
... ... @@ -634,7 +634,7 @@
634 634   return view(xwikiDocument, true).then(editInPlace);
635 635   };
636 636  
637 - var view = function(xwikiDocument, reload) {
636 + async function view(xwikiDocument, reload) {
638 638   if (xwikiDocument.isNew && xwikiDocument.language && xwikiDocument.defaultTranslation) {
639 639   // The user tried to translate the current document in the UI locale and either canceled the edit without saving
640 640   // or there was an error. Display the default translation to the user.
... ... @@ -641,29 +641,51 @@
641 641   xwikiDocument = xwikiDocument.defaultTranslation;
642 642   }
643 643  
644 - var viewContent = $('#xwikicontent');
645 - // Destroy the editors before returning to view.
646 - viewContent.trigger('xwiki:actions:view', {document: xwikiDocument});
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 + );
647 647   $('#document-title h1').html(xwikiDocument.renderedTitle);
648 648   viewContent.html(xwikiDocument.renderedContent);
655 +
649 649   // Reset the editor to let others know that we're not editing anymore.
650 650   XWiki.editor = '';
658 +
651 651   if (!reload) {
652 652   // If the user has canceled the edit then the restored page content may include the section edit links. Show them
653 653   // in case they were hidden.
654 654   viewContent.children(':header').children('.edit_section').removeClass('hidden');
663 +
655 655   // Let others know that the DOM has been updated, in order to enhance it.
656 656   $(document).trigger('xwiki:dom:updated', {'elements': viewContent.toArray()});
657 657   }
667 +
658 658   // Remove the action events scope.
659 659   viewContent.closest('.form').removeClass('form');
670 +
660 660   // Update the URL.
661 661   if (window.location.hash === '#edit' || window.location.hash === '#translate') {
662 662   history.replaceState(null, null, '#');
663 663   }
664 - return Promise.resolve(xwikiDocument);
665 - };
666 666  
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 +
667 667   var edit = function(xwikiDocument) {
668 668   // By adding the 'form' CSS class we set the scope of the action events (e.g. xwiki:actions:beforeSave or
669 669   // xwiki:actions:cancel). We need this because in view mode we can have multiple forms active on the page (e.g. one
... ... @@ -692,9 +692,6 @@
692 692   <div hidden>
693 693   <input type="hidden" name="form_token" />
694 694   <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" />
698 698   <input type="hidden" name="language" />
699 699   </div>
700 700   <fieldset id="xwikieditcontent" class="xform inplace-editing-buttons sticky-buttons"></fieldset>
... ... @@ -815,16 +815,6 @@
815 815   form.find('input[name="language"]').val(xwikiDocument.getRealLocale());
816 816   form.find('input[name="isNew"]').val(xwikiDocument.isNew);
817 817  
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 -
828 828   // Check for merge conflicts only if the document is not new and we know the current version.
829 829   if (!xwikiDocument.isNew && xwikiDocument.version) {
830 830   form.find('input[name="previousVersion"]').val(xwikiDocument.version);
... ... @@ -836,14 +836,13 @@
836 836   var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype);
837 837   $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, {
838 838   reloadEditor: function() {
839 - var actionButtons = $('.inplace-editing-buttons');
840 - if (actionButtons.is(':visible')) {
847 + if (XWiki.editor === config.editMode) {
841 841   // This function is called after the document save confirmation is received, if the save was done by merge. We
842 842   // register our reload listener from a document saved listener, but we're using promises which are
843 843   // asynchronous so the reload listener is actually registered with a delay. For this reason we trigger the
844 844   // reload event with a delay to ensure our reload listener is called.
845 845   setTimeout(function() {
846 - actionButtons.trigger('xwiki:actions:reload');
853 + $('.inplace-editing-buttons').trigger('xwiki:actions:reload');
847 847   }, 0);
848 848   } else {
849 849   return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments);
... ... @@ -850,13 +850,13 @@
850 850   }
851 851   },
852 852   maybeRedirect: function(continueEditing) {
853 - if ($('.inplace-editing-buttons').is(':visible')) {
860 + if (XWiki.editor === config.editMode) {
854 854   // Overwrite the default behavior so that we don't redirect when leaving the edit mode because we're already
855 855   // in view mode. We still need to report a redirect (return true) if we don't continue editing, so that
856 856   // actionButtons.js behaves as if a redirect was done.
857 857   return !continueEditing;
858 858   } else {
859 - // Fallback on the default behavior if the in-place editing buttons are hidden.
866 + // Fallback on the default behavior if we're not editing in-place.
860 860   return originalAjaxSaveAndContinue.maybeRedirect.apply(this, arguments);
861 861   }
862 862   }
... ... @@ -899,9 +899,7 @@
899 899   var withFocus = document.activeElement && document.activeElement === editContent[0];
900 900   // Keep showing the view content until the edit content is ready in order to avoid UI flicker.
901 901   var viewContent = editContent.clone().insertAfter(editContent);
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);
909 + editContent.hide().empty();
905 905   if (withFocus) {
906 906   // Keep the focus while the edit content is being prepared.
907 907   viewContent.focus();
... ... @@ -910,6 +910,8 @@
910 910   // Use the same name as for the standalone editor, in order to be consistent.
911 911   editorName: 'content',
912 912   document: xwikiDocument,
918 + startupFocus: withFocus,
919 + formId: 'inplace-editing',
913 913   // The content editor is loaded on demand, asynchronously.
914 914   deferred: $.Deferred()
915 915   });
... ... @@ -917,14 +917,6 @@
917 917   return data.deferred.promise().then(() => {
918 918   editContent.show();
919 919   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 - }
928 928   return xwikiDocument;
929 929   });
930 930   };
... ... @@ -939,12 +939,15 @@
939 939   }
940 940   };
941 941  
942 - return {
941 + let module = {
942 + contentEditor: undefined,
943 943   preload,
944 944   editPage,
945 945   editSection,
946 946   translatePage
947 947   };
948 +
949 + return module;
948 948  });
949 949  
950 950  require(['jquery'], function($) {
... ... @@ -953,16 +953,15 @@
953 953   return;
954 954   }
955 955  
956 - var wysiwygEditorModule = 'xwiki-' + config.wysiwygEditor + '-inline';
957 -
958 958   var preloadEditor = function() {
959 - require(['editInPlace', wysiwygEditorModule], function(editInPlace) {
959 + require(['editInPlace', config.contentEditor], function(editInPlace, contentEditor) {
960 + editInPlace.contentEditor = contentEditor;
960 960   editInPlace.preload();
961 961   // Fallback on the standalone edit mode if we fail to load the required modules.
962 962   }, disableInPlaceEditing);
963 963   };
964 964  
965 - // Preload the WYSIWYG editor code without slowing down the page view.
966 + // Preload the content editor code without slowing down the page view.
966 966   if (document.readyState === 'complete') {
967 967   setTimeout(preloadEditor, 0);
968 968   } else {
... ... @@ -1016,7 +1016,8 @@
1016 1016   const data = handler.beforeEdit?.(event);
1017 1017   // Load the code needed to edit in place only when the edit button is clicked.
1018 1018   currentlyEditing = new Promise((resolve, reject) => {
1019 - require(['editInPlace', wysiwygEditorModule], (editInPlace) => {
1020 + require(['editInPlace', config.contentEditor], (editInPlace, contentEditor) => {
1021 + editInPlace.contentEditor = contentEditor;
1020 1020   // Re-enable the translate button because it can be used while editing to create the missing translation.
1021 1021   translateButton.removeClass('disabled');
1022 1022   handler.edit(editInPlace, data).finally(() => {
XWiki.StyleSheetExtension[0]
Code
... ... @@ -1,29 +27,3 @@
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,
27 27  #xwikicontent[contenteditable]:focus,
28 28  #xwikicontent[tabindex]:focus {
29 29   outline: 0;
XWiki.UIExtensionClass[0]
Executed Content
... ... @@ -39,16 +39,12 @@
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).
45 45   #set ($inplaceEditingConfig = {
46 46   'contentType': 'org.xwiki.rendering.syntax.SyntaxContent',
47 47   'editMode': $defaultEditMode,
48 - 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id,
45 + 'contentEditor': "xwiki-${services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id}-inline",
49 49   'editButtonSelector': '#tmEdit > a',
50 50   'translateButtonSelector': '#tmTranslate > a',
51 - 'enableSourceMode': true,
52 52   'paths': {
53 53   'js': {
54 54   'xwiki-actionButtons': "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.js' $jsParams)",