Changes for page InplaceEditing

From version 6.1
edited by Nazzareno Pompei
on 19/01/2024 08:57
Change comment: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/15.10.5]
To version 4.1
edited by Nazzareno Pompei
on 28/10/2022 08:43
Change comment: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/14.9]

Summary

Details

XWiki.JavaScriptExtension[0]
Code
... ... @@ -223,7 +223,7 @@
223 223   };
224 224  
225 225   var loadCSS = function(url) {
226 - $('<link/>').attr({
226 + var link = $('<link>').attr({
227 227   type: 'text/css',
228 228   rel: 'stylesheet',
229 229   href: url
... ... @@ -292,8 +292,7 @@
292 292   options.afterEdit(xwikiDocument);
293 293   return xwikiDocument;
294 294   }).finally(() => {
295 - // Remove the aria-expanded attribute which is incorrect for role=textbox
296 - $('#xwikicontent').removeClass('loading').removeAttr('aria-expanded');
295 + $('#xwikicontent').removeClass('loading');
297 297   // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly.
298 298   }).then(maybeSave)
299 299   // Then unlock the document both when the edit ended with success and with a failure.
... ... @@ -581,41 +581,28 @@
581 581   // Thus we need to use the grid for the sticky buttons also otherwise the postion is badly computed when scrolling
582 582   // (because of the float on the previous element). This wouldn't be needed if we were using position:sticky, which
583 583   // we can't use yet because it's not implemented on IE11 which we still have to support.
584 - let inplaceEditingForm = editContent.nextAll('form#inplace-editing');
585 - if (!inplaceEditingForm.length) {
586 - // The 'xwikieditcontent' id is needed for the auto-save feature (otherwise it doesn't find the form).
587 - inplaceEditingForm = $(`
588 - <form id="inplace-editing" class="col-xs-12">
589 - <div hidden>
590 - <input type="hidden" name="form_token" />
591 - <input type="hidden" name="async" value="true" />
592 - <input type="hidden" name="content" />
593 - <input type="hidden" name="RequiresHTMLConversion" value="content" />
594 - <input type="hidden" name="content_syntax" />
595 - <input type="hidden" name="language" />
596 - </div>
597 - <fieldset id="xwikieditcontent" class="xform inplace-editing-buttons sticky-buttons"></fieldset>
598 - </form>
599 - `).attr('action', XWiki.currentDocument.getURL('save'))
600 - .insertAfter(editContent).toggle(!!xwikiDocument);
601 - inplaceEditingForm.find('input[name="form_token"]').val(xcontext.form_token);
602 - var actionButtons = inplaceEditingForm.children('.sticky-buttons').data('xwikiDocument', xwikiDocument);
583 + var actionButtonsWrapper = editContent.nextAll('.sticky-buttons-wrapper');
584 + if (actionButtonsWrapper.length === 0) {
585 + actionButtonsWrapper = $('<div class="sticky-buttons-wrapper col-xs-12">' +
586 + '<div class="inplace-editing-buttons sticky-buttons"/></div>').insertAfter(editContent).toggle(!!xwikiDocument);
587 + var actionButtons = actionButtonsWrapper.children('.sticky-buttons')
588 + .data('xwikiDocument', xwikiDocument)
589 + // Expose the fake form if an extension needs to manipulate it.
590 + .data('fakeForm', fakeForm);
603 603   return loadActionButtons(actionButtons);
604 604   } else {
605 605   // If we're editing a page..
606 606   if (xwikiDocument) {
607 607   // ..then make sure the action buttons are displayed right away (don't wait for the user to scroll).
608 - inplaceEditingForm.show().children('.sticky-buttons')
596 + actionButtonsWrapper.show().children('.sticky-buttons')
609 609   .data('xwikiDocument', xwikiDocument)
610 - // Make sure the position of the action buttons is updated.
611 - .trigger('xwiki:dom:refresh')
612 - // The action buttons are disabled on Save & View. We don't reload the page on Save & View and we reuse the
613 - // action buttons so we need to re-enable them each time we enter the edit mode.
614 - .prop('disabled', false);
615 - // Cleanup the extra hidden input fields that actionButtons.js might have appended to the form. We have to do
616 - // this each time the form is (re)enabled (i.e. after a failed Save & View or before entering the edit mode)
617 - // because they are designed to be used once.
618 - inplaceEditingForm.children('fieldset').nextAll().remove();
598 + // Expose the fake form if an extension needs to manipulate it.
599 + .data('fakeForm', fakeForm)
600 + // but make sure the position of the action buttons is updated.
601 + .trigger('xwiki:dom:refresh');
602 + // The action buttons are disabled on Save & View. We don't reload the page on Save & View and we reuse the
603 + // action buttons so we need to re-enable them each time we enter the edit mode.
604 + fakeForm.enable();
619 619   }
620 620   return Promise.resolve(xwikiDocument);
621 621   }
... ... @@ -657,17 +657,6 @@
657 657   };
658 658  
659 659   var loadActionButtons = function(actionButtons) {
660 - // We want to update the form data as late as possible (but still before the form is validated), in order to allow
661 - // the title and content editors to update their values and the 'xwikiDocument' instance. We do this by catching the
662 - // event early (lower in the DOM, at the start of the event bubbling phase) and adding a one time event listener for
663 - // the end of the event bubbling phase at the top level of the DOM document.
664 - actionButtons.on('xwiki:actions:beforeSave', function() {
665 - $(document).one('xwiki:actions:beforeSave', updateFormDataBeforeSave);
666 - });
667 - actionButtons.on('xwiki:actions:cancel', function(event) {
668 - // We are already in view mode so there's no need to leave the page.
669 - event.preventDefault();
670 - });
671 671   $(document).on('xwiki:actions:view', '.xcontent.form', function(event, data) {
672 672   // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit
673 673   // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing
... ... @@ -674,7 +674,7 @@
674 674   // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action
675 675   // buttons to have them ready for the next editing session (the user can save or cancel and then edit again
676 676   // without reloading the page).
677 - actionButtons.find(':input').blur().end().prop('disabled', true).parent().hide();
652 + actionButtons.find(':input').blur().prop('disabled', true).end().parent().hide();
678 678   // Restore the Translate button if the locale of the viewed document doesn't match the current user interface
679 679   // locale (because the viewed document doesn't have a translation in the current locale).
680 680   var xwikiDocumentLocale = data.document.getRealLocale();
... ... @@ -695,14 +695,23 @@
695 695   actionButtons.html(html);
696 696   // Fix the name of the Save & View action.
697 697   actionButtons.find('.btn-primary').first().attr('name', 'action_save');
673 + // Append the hidden input field that keeps the CSRF token.
674 + $('<input type="hidden" name="form_token" />').val(xcontext.form_token).appendTo(actionButtons);
675 + // We need a place where actionButtons.js can add more hidden inputs.
676 + actionButtons.append('<div class="hidden extra"/>');
698 698   // Let the others know that the DOM has been updated, in order to enhance it.
699 699   $(document).trigger('xwiki:dom:updated', {'elements': actionButtons.toArray()});
700 700   return new Promise((resolve, reject) => {
701 701   require(['xwiki-actionButtons', 'xwiki-diff', 'xwiki-autoSave'], function() {
681 + overrideEditActions();
702 702   overrideAjaxSaveAndContinue();
683 + // Activate the auto-save feature passing our fake edit form. Note that autosave.js also creates an instance of
684 + // AutoSave but it doesn't do anything because it doesn't find a real edit form in the page. This is why we have
685 + // to create our own instance of AutoSave passing the right (fake) form.
686 + new XWiki.editors.AutoSave({form: fakeForm});
703 703   var xwikiDocument = actionButtons.data('xwikiDocument');
704 704   // Enable the action buttons (and their shortcut keys) only if we're editing a document.
705 - actionButtons.prop('disabled', !xwikiDocument);
689 + actionButtons.find(':input').prop('disabled', !xwikiDocument);
706 706   resolve(xwikiDocument);
707 707   });
708 708   });
... ... @@ -711,30 +711,92 @@
711 711   });
712 712   };
713 713  
714 - var updateFormDataBeforeSave = function() {
715 - const form = $('form#inplace-editing');
716 - const xwikiDocument = form.children('.sticky-buttons').data('xwikiDocument');
717 -
718 - form.find('input[name="language"]').val(xwikiDocument.getRealLocale());
719 - form.find('input[name="isNew"]').val(xwikiDocument.isNew);
720 -
721 - // Submit either the raw (source) content (no syntax conversion needed in this case) or the rendered content (HTML)
722 - // in which case we have to force the conversion to the document syntax on the server.
723 - const submitRawContent = xwikiDocument.content !== xwikiDocument.originalDocument.content;
724 - form.find('input[name="content"]').val(submitRawContent ? xwikiDocument.content : xwikiDocument.renderedContent);
725 - form.find('input[name="RequiresHTMLConversion"]').prop('disabled', submitRawContent);
726 - form.find('input[name="content_syntax"]').val(xwikiDocument.syntax).prop('disabled', submitRawContent);
727 -
728 - // Add the temporary uploaded files to the form.
729 - $('#xwikicontent').nextAll('input[name="uploadedFiles"]').attr('form', 'inplace-editing');
730 -
731 - // Check for merge conflicts only if the document is not new and we know the current version.
732 - if (!xwikiDocument.isNew && xwikiDocument.version) {
733 - form.find('input[name="previousVersion"]').val(xwikiDocument.version);
734 - form.find('input[name="editingVersionDate"]').val(new Date(xwikiDocument.modified).getTime());
698 + // actionButtons.js expects a form so we use a fake form. Refactoring actionButtons.js is too dangerous ATM.
699 + var fakeForm = {
700 + action: XWiki.currentDocument.getURL('save'),
701 + async: true,
702 + _getActionButtons: function() {
703 + if (!this._actionButtons) {
704 + this._actionButtons = $('#xwikicontent').nextAll('.sticky-buttons-wrapper').children('.sticky-buttons');
705 + }
706 + return this._actionButtons;
707 + },
708 + disable: function() {
709 + this._getActionButtons().find(':input').prop('disabled', true);
710 + },
711 + enable: function() {
712 + // Clear the extra hidden input fields, that actionButtons.js might have added, each time the form is (re)enabled
713 + // (i.e. after a failed Save & View or before entering the edit mode) because they are designed to be used once.
714 + this._getActionButtons().find('.hidden.extra').empty();
715 + this._getActionButtons().find(':input').prop('disabled', false);
716 + },
717 + insert: function(element) {
718 + this._getActionButtons().find('.hidden.extra').append(element);
719 + },
720 + // Note that this method only works with single argument.
721 + append: function(element) {
722 + this.insert(element);
723 + },
724 + down: function(selector) {
725 + return this._getActionButtons().find(selector)[0];
726 + },
727 + serialize: function() {
728 + var extra = this._getActionButtons().find(':input').serializeArray().reduce(function(extra, entry) {
729 + var value = extra[entry.name] || [];
730 + value.push(entry.value);
731 + extra[entry.name] = value;
732 + return extra;
733 + }, {});
734 + // retrieve all input fields listing the temporary uploaded files.
735 + var uploadedFiles = $('#xwikicontent').nextAll('input[name="uploadedFiles"]').serializeArray().reduce(function(extra, entry) {
736 + var value = extra[entry.name] || [];
737 + value.push(entry.value);
738 + extra[entry.name] = value;
739 + return extra;
740 + }, {});
741 + var xwikiDocument = this._getActionButtons().data('xwikiDocument');
742 + var formData = {
743 + title: xwikiDocument.rawTitle,
744 + language: xwikiDocument.getRealLocale(),
745 + isNew: xwikiDocument.isNew
746 + };
747 + if (xwikiDocument.content != xwikiDocument.originalDocument.content) {
748 + // Submit the raw (source) content. No syntax conversion is needed in this case.
749 + formData.content = xwikiDocument.content;
750 + } else {
751 + // Submit the rendered content (HTML), but make sure it is converted to the document syntax on the server.
752 + $.extend(formData, {
753 + content: xwikiDocument.renderedContent,
754 + RequiresHTMLConversion: 'content',
755 + content_syntax: xwikiDocument.syntax
756 + });
757 + }
758 + // Add the temporary uploaded files to the form.
759 + $.extend(formData, uploadedFiles);
760 + // Check for merge conflicts only if the document is not new and we know the current version.
761 + if (!xwikiDocument.isNew && xwikiDocument.version) {
762 + formData.previousVersion = xwikiDocument.version;
763 + formData.editingVersionDate = new Date(xwikiDocument.modified).getTime();
764 + }
765 + // Ensure that formData information has priority over extra information.
766 + return $.extend({}, extra, formData);
735 735   }
736 736   };
737 737  
770 + var overrideEditActions = function() {
771 + // Override the EditActions.notify() function in order to pass a fake form in the event parameters.
772 + var originalNotify = XWiki.actionButtons.EditActions.prototype.notify;
773 + XWiki.actionButtons.EditActions.prototype.notify = function(originalEvent, action, params) {
774 + if (params && $(originalEvent.element()).closest('.inplace-editing-buttons').length > 0) {
775 + // actionButtons.js expects a form so we use a fake form. Refactoring actionButtons.js is too dangerous ATM.
776 + // Note that we do this only when the event has parameters because we want to exclude the cancel event for which
777 + // actionButtons.js changes the window location if a form is specified, and we want to prevent that.
778 + params.form = fakeForm;
779 + }
780 + return originalNotify.apply(this, arguments);
781 + };
782 + };
783 +
738 738   var overrideAjaxSaveAndContinue = function() {
739 739   var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype);
740 740   $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, {
... ... @@ -741,13 +741,7 @@
741 741   reloadEditor: function() {
742 742   var actionButtons = $('.inplace-editing-buttons');
743 743   if (actionButtons.is(':visible')) {
744 - // This function is called after the document save confirmation is received, if the save was done by merge. We
745 - // register our reload listener from a document saved listener, but we're using promises which are
746 - // asynchronous so the reload listener is actually registered with a delay. For this reason we trigger the
747 - // reload event with a delay to ensure our reload listener is called.
748 - setTimeout(function() {
749 - actionButtons.trigger('xwiki:actions:reload');
750 - }, 0);
790 + actionButtons.trigger('xwiki:actions:reload');
751 751   } else {
752 752   return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments);
753 753   }
... ... @@ -769,20 +769,12 @@
769 769   var initTitleEditor = function(xwikiDocument) {
770 770   var label = $('<label for="document-title-input" class="sr-only"/>')
771 771   .text(l10n['core.editors.content.titleField.label']);
772 - var input = $('<input type="text" id="document-title-input" name="title" form="inplace-editing" />')
773 - .val(xwikiDocument.rawTitle);
774 - if (config.titleIsMandatory) {
775 - input.attr({
776 - 'required': '',
777 - 'data-validation-value-missing': l10n['core.validation.required.message']
778 - });
779 - } else {
780 - var placeholder = xwikiDocument.documentReference.name;
781 - if (placeholder === 'WebHome') {
782 - placeholder = xwikiDocument.documentReference.parent.name;
783 - }
784 - input.attr('placeholder', placeholder);
812 + var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.rawTitle);
813 + var placeholder = xwikiDocument.documentReference.name;
814 + if (placeholder === 'WebHome') {
815 + placeholder = xwikiDocument.documentReference.parent.name;
785 785   }
817 + input.attr('placeholder', placeholder);
786 786   $('#document-title h1').addClass('editable').empty().append([label, input]);
787 787   $(document).on('xwiki:actions:beforeSave.titleEditor', '.xcontent.form', function(event) {
788 788   xwikiDocument.rawTitle = input.val();
... ... @@ -882,10 +882,6 @@
882 882   }
883 883   // Disable the edit buttons and hide the section edit links.
884 884   editButton.add(translateButton).addClass('disabled');
885 - editButton.attr('aria-disabled', 'true');
886 - var reference = editButton.attr('href');
887 - editButton.removeAttr('href');
888 - editButton.attr('role', 'link');
889 889   $('#xwikicontent').children(':header').children('.edit_section').addClass('hidden');
890 890   event.preventDefault();
891 891   const handler = event.data;
... ... @@ -899,9 +899,6 @@
899 899   // * the translate button is restored (if needed) by the editInPlace module
900 900   // * the section edit links are restored when the document is rendered for view
901 901   editButton.removeClass('disabled');
902 - editButton.removeAttr('aria-disabled');
903 - editButton.removeAttr('role');
904 - editButton.attr('href', reference);
905 905   });
906 906   // Fallback on the standalone edit mode if we fail to load the required modules.
907 907   }, disableInPlaceEditing.bind(event.target));
XWiki.StyleSheetExtension[0]
Code
... ... @@ -9,9 +9,10 @@
9 9  @document-title-input-padding-vertical: @line-height-computed / 4 - 1;
10 10  input#document-title-input {
11 11   /* Preserve the heading styles. */
12 + border: 1px solid transparent;
13 + box-shadow: none;
12 12   color: inherit;
13 13   font-size: inherit;
14 - background-color: @body-bg;
15 15   /* It seems it's not enough to set the line height for the text input. We also need to set its height. */
16 16   height: @font-size-document-title * @headings-line-height + 2 * (1 + @document-title-input-padding-vertical);
17 17   line-height: @headings-line-height;
... ... @@ -18,16 +18,12 @@
18 18   padding: @document-title-input-padding-vertical (ceil(@grid-gutter-width / 2) - 1);
19 19   width: 100%;
20 20  }
21 -input#document-title-input:valid {
22 - border: 1px solid transparent;
23 - box-shadow: none;
24 -}
25 25  
26 -input#document-title-input:valid:hover {
23 +input#document-title-input:hover {
27 27   border-color: @input-border;
28 28  }
29 29  
30 -input#document-title-input:valid:focus,
27 +input#document-title-input:focus,
31 31  #xwikicontent[contenteditable]:focus,
32 32  #xwikicontent[tabindex]:focus {
33 33   .form-control-focus();
... ... @@ -53,7 +53,7 @@
53 53   padding-top: @line-height-computed * 0.75;
54 54  }
55 55  
56 -form#inplace-editing {
53 +.sticky-buttons-wrapper {
57 57   /* Leave some space for the bottom box shadow of the editing area. */
58 58   margin-top: 7px;
59 59  }
XWiki.UIExtensionClass[0]
Executed Content
... ... @@ -19,7 +19,6 @@
19 19   'edit.inplace.page.loadFailed',
20 20   'edit.inplace.actionButtons.loadFailed',
21 21   'core.editors.content.titleField.label',
22 - 'core.validation.required.message',
23 23   ['edit.inplace.page.translate.messageBefore', $doc.realLocale.getDisplayName($xcontext.locale),
24 24   $xcontext.locale.getDisplayName($xcontext.locale)],
25 25   ['edit.inplace.page.translate.messageAfter', $xcontext.locale.getDisplayName($xcontext.locale)]
... ... @@ -63,7 +63,6 @@
63 63   },
64 64   'l10n': $l10n
65 65   })
66 - #set ($inplaceEditingConfig.titleIsMandatory = $xwiki.getSpacePreference('xwiki.title.mandatory') == 1)
67 67   <div class="hidden" data-inplace-editing-config="$escapetool.xml($jsontool.serialize($inplaceEditingConfig))"></div>
68 68   ## We didn't move this to the file system because it uses LESS and we didn't want to include it in the skin.
69 69   #set ($discard = $xwiki.ssx.use('XWiki.InplaceEditing'))