Changes for page InplaceEditing
From version 4.1
edited by Nazzareno Pompei
on 28/10/2022 08:43
on 28/10/2022 08:43
Change comment:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/14.9]
To version 6.1
edited by Nazzareno Pompei
on 19/01/2024 08:57
on 19/01/2024 08:57
Change comment:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/15.10.5]
Summary
-
Objects (3 modified, 0 added, 0 removed)
Details
- XWiki.JavaScriptExtension[0]
-
- Code
-
... ... @@ -223,7 +223,7 @@ 223 223 }; 224 224 225 225 var loadCSS = function(url) { 226 - var link =$('<link>').attr({226 + $('<link/>').attr({ 227 227 type: 'text/css', 228 228 rel: 'stylesheet', 229 229 href: url ... ... @@ -292,7 +292,8 @@ 292 292 options.afterEdit(xwikiDocument); 293 293 return xwikiDocument; 294 294 }).finally(() => { 295 - $('#xwikicontent').removeClass('loading'); 295 + // Remove the aria-expanded attribute which is incorrect for role=textbox 296 + $('#xwikicontent').removeClass('loading').removeAttr('aria-expanded'); 296 296 // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly. 297 297 }).then(maybeSave) 298 298 // Then unlock the document both when the edit ended with success and with a failure. ... ... @@ -580,28 +580,41 @@ 580 580 // Thus we need to use the grid for the sticky buttons also otherwise the postion is badly computed when scrolling 581 581 // (because of the float on the previous element). This wouldn't be needed if we were using position:sticky, which 582 582 // we can't use yet because it's not implemented on IE11 which we still have to support. 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); 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); 591 591 return loadActionButtons(actionButtons); 592 592 } else { 593 593 // If we're editing a page.. 594 594 if (xwikiDocument) { 595 595 // ..then make sure the action buttons are displayed right away (don't wait for the user to scroll). 596 - acti onButtonsWrapper.show().children('.sticky-buttons')608 + inplaceEditingForm.show().children('.sticky-buttons') 597 597 .data('xwikiDocument', xwikiDocument) 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(); 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(); 605 605 } 606 606 return Promise.resolve(xwikiDocument); 607 607 } ... ... @@ -643,6 +643,17 @@ 643 643 }; 644 644 645 645 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 + }); 646 646 $(document).on('xwiki:actions:view', '.xcontent.form', function(event, data) { 647 647 // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit 648 648 // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing ... ... @@ -649,7 +649,7 @@ 649 649 // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action 650 650 // buttons to have them ready for the next editing session (the user can save or cancel and then edit again 651 651 // without reloading the page). 652 - actionButtons.find(':input').blur().prop('disabled', true). end().parent().hide();677 + actionButtons.find(':input').blur().end().prop('disabled', true).parent().hide(); 653 653 // Restore the Translate button if the locale of the viewed document doesn't match the current user interface 654 654 // locale (because the viewed document doesn't have a translation in the current locale). 655 655 var xwikiDocumentLocale = data.document.getRealLocale(); ... ... @@ -670,23 +670,14 @@ 670 670 actionButtons.html(html); 671 671 // Fix the name of the Save & View action. 672 672 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"/>'); 677 677 // Let the others know that the DOM has been updated, in order to enhance it. 678 678 $(document).trigger('xwiki:dom:updated', {'elements': actionButtons.toArray()}); 679 679 return new Promise((resolve, reject) => { 680 680 require(['xwiki-actionButtons', 'xwiki-diff', 'xwiki-autoSave'], function() { 681 - overrideEditActions(); 682 682 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}); 687 687 var xwikiDocument = actionButtons.data('xwikiDocument'); 688 688 // Enable the action buttons (and their shortcut keys) only if we're editing a document. 689 - actionButtons. find(':input').prop('disabled', !xwikiDocument);705 + actionButtons.prop('disabled', !xwikiDocument); 690 690 resolve(xwikiDocument); 691 691 }); 692 692 }); ... ... @@ -695,92 +695,30 @@ 695 695 }); 696 696 }; 697 697 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); 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()); 767 767 } 768 768 }; 769 769 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 - 784 784 var overrideAjaxSaveAndContinue = function() { 785 785 var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype); 786 786 $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, { ... ... @@ -787,7 +787,13 @@ 787 787 reloadEditor: function() { 788 788 var actionButtons = $('.inplace-editing-buttons'); 789 789 if (actionButtons.is(':visible')) { 790 - actionButtons.trigger('xwiki:actions:reload'); 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); 791 791 } else { 792 792 return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments); 793 793 } ... ... @@ -809,12 +809,20 @@ 809 809 var initTitleEditor = function(xwikiDocument) { 810 810 var label = $('<label for="document-title-input" class="sr-only"/>') 811 811 .text(l10n['core.editors.content.titleField.label']); 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; 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); 816 816 } 817 - input.attr('placeholder', placeholder); 818 818 $('#document-title h1').addClass('editable').empty().append([label, input]); 819 819 $(document).on('xwiki:actions:beforeSave.titleEditor', '.xcontent.form', function(event) { 820 820 xwikiDocument.rawTitle = input.val(); ... ... @@ -914,6 +914,10 @@ 914 914 } 915 915 // Disable the edit buttons and hide the section edit links. 916 916 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'); 917 917 $('#xwikicontent').children(':header').children('.edit_section').addClass('hidden'); 918 918 event.preventDefault(); 919 919 const handler = event.data; ... ... @@ -927,6 +927,9 @@ 927 927 // * the translate button is restored (if needed) by the editInPlace module 928 928 // * the section edit links are restored when the document is rendered for view 929 929 editButton.removeClass('disabled'); 902 + editButton.removeAttr('aria-disabled'); 903 + editButton.removeAttr('role'); 904 + editButton.attr('href', reference); 930 930 }); 931 931 // Fallback on the standalone edit mode if we fail to load the required modules. 932 932 }, disableInPlaceEditing.bind(event.target));
- XWiki.StyleSheetExtension[0]
-
- Code
-
... ... @@ -9,10 +9,9 @@ 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; 14 14 color: inherit; 15 15 font-size: inherit; 14 + background-color: @body-bg; 16 16 /* It seems it's not enough to set the line height for the text input. We also need to set its height. */ 17 17 height: @font-size-document-title * @headings-line-height + 2 * (1 + @document-title-input-padding-vertical); 18 18 line-height: @headings-line-height; ... ... @@ -19,12 +19,16 @@ 19 19 padding: @document-title-input-padding-vertical (ceil(@grid-gutter-width / 2) - 1); 20 20 width: 100%; 21 21 } 21 +input#document-title-input:valid { 22 + border: 1px solid transparent; 23 + box-shadow: none; 24 +} 22 22 23 -input#document-title-input:hover { 26 +input#document-title-input:valid:hover { 24 24 border-color: @input-border; 25 25 } 26 26 27 -input#document-title-input:focus, 30 +input#document-title-input:valid:focus, 28 28 #xwikicontent[contenteditable]:focus, 29 29 #xwikicontent[tabindex]:focus { 30 30 .form-control-focus(); ... ... @@ -50,7 +50,7 @@ 50 50 padding-top: @line-height-computed * 0.75; 51 51 } 52 52 53 - .sticky-buttons-wrapper{56 +form#inplace-editing { 54 54 /* Leave some space for the bottom box shadow of the editing area. */ 55 55 margin-top: 7px; 56 56 }
- XWiki.UIExtensionClass[0]
-
- Executed Content
-
... ... @@ -19,6 +19,7 @@ 19 19 'edit.inplace.page.loadFailed', 20 20 'edit.inplace.actionButtons.loadFailed', 21 21 'core.editors.content.titleField.label', 22 + 'core.validation.required.message', 22 22 ['edit.inplace.page.translate.messageBefore', $doc.realLocale.getDisplayName($xcontext.locale), 23 23 $xcontext.locale.getDisplayName($xcontext.locale)], 24 24 ['edit.inplace.page.translate.messageAfter', $xcontext.locale.getDisplayName($xcontext.locale)] ... ... @@ -62,6 +62,7 @@ 62 62 }, 63 63 'l10n': $l10n 64 64 }) 66 + #set ($inplaceEditingConfig.titleIsMandatory = $xwiki.getSpacePreference('xwiki.title.mandatory') == 1) 65 65 <div class="hidden" data-inplace-editing-config="$escapetool.xml($jsontool.serialize($inplaceEditingConfig))"></div> 66 66 ## We didn't move this to the file system because it uses LESS and we didn't want to include it in the skin. 67 67 #set ($discard = $xwiki.ssx.use('XWiki.InplaceEditing'))