Changes for page InplaceEditing

From version 1.1
edited by N Pompei
on 08/06/2020 17:32
Change comment: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/12.4]
To version 2.1
edited by N Pompei
on 16/09/2020 12:29
Change comment: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/12.7.1]

Summary

Details

XWiki.JavaScriptExtension[0]
Code
... ... @@ -1,8 +1,9 @@
1 1  require.config({
2 2   paths: {
3 3   'actionButtons': $jsontool.serialize($xwiki.getSkinFile('js/xwiki/actionbuttons/actionButtons.js', true)),
4 + 'autoSave': $jsontool.serialize($xwiki.getSkinFile('js/xwiki/editors/autosave.js', true)),
4 4   // Required in case the user needs to resolve merge conflicts on save.
5 - 'diff': $jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.js'))
6 + 'xwiki-diff': $jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.js'))
6 6   }
7 7  });
8 8  
... ... @@ -17,7 +17,17 @@
17 17   * @return this document's real locale
18 18   */
19 19   getRealLocale: function() {
20 - return this.language || (this.translations && this.translations['default']) || $('html').attr('lang');
21 + var realLocale = this.language;
22 + if (typeof realLocale === 'string' && realLocale !== '') {
23 + // This document is a translation.
24 + } else if (this.translations && typeof this.translations['default'] === 'string') {
25 + // This is the original document.
26 + realLocale = this.translations['default'];
27 + } else {
28 + // The document locale is not specified. Use the UI locale.
29 + realLocale = $('html').attr('lang');
30 + }
31 + return realLocale;
21 21   },
22 22  
23 23   /**
... ... @@ -55,7 +55,7 @@
55 55   */
56 56   render: function(forView) {
57 57   var queryString = {
58 - xpage: 'rendercontent',
69 + xpage: 'get',
59 59   outputTitle: true,
60 60   outputSyntax: forView ? null : 'annotatedxhtml',
61 61   language: this.getRealLocale(),
... ... @@ -69,11 +69,15 @@
69 69   'error'
70 70   );
71 71   }).then(function(html) {
83 + // Render succeeded.
72 72   var container = $('<div/>').html(html);
73 73   return $.extend(thisXWikiDocument, {
74 74   renderedTitle: container.find('#document-title h1').html(),
75 75   renderedContent: container.find('#xwikicontent').html()
76 76   });
89 + }, function() {
90 + // Render failed.
91 + return thisXWikiDocument;
77 77   });
78 78   },
79 79  
... ... @@ -88,11 +88,15 @@
88 88   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
89 89   timestamp: new Date().getTime()
90 90   }).then(function(newXWikiDocument) {
106 + // Reload succeeded.
91 91   // Resolve the document reference.
92 92   thisXWikiDocument.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);
93 93   // We were able to load the document so it's not new.
94 94   thisXWikiDocument.isNew = false;
95 95   return $.extend(thisXWikiDocument, newXWikiDocument);
112 + }, function() {
113 + // Reload failed.
114 + return thisXWikiDocument;
96 96   });
97 97   },
98 98  
... ... @@ -115,8 +115,19 @@
115 115   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
116 116   timestamp: new Date().getTime()
117 117   }).then(function() {
137 + // Lock succeeded.
118 118   thisXWikiDocument.locked = action;
119 119   return thisXWikiDocument;
140 + }, function(response) {
141 + // Lock failed.
142 + delete thisXWikiDocument.locked;
143 + // Check if the user can force the lock.
144 + var lockConfirmation = response.responseJSON;
145 + if (response.status === 423 && lockConfirmation) {
146 + // The user can force the lock, but needs confirmation.
147 + thisXWikiDocument.lockConfirmation = lockConfirmation;
148 + }
149 + return thisXWikiDocument;
120 120   });
121 121   },
122 122  
... ... @@ -156,6 +156,7 @@
156 156  ], function($, xcontext, xwikiDocumentAPI) {
157 157   var preload = function() {
158 158   loadCSS($jsontool.serialize($xwiki.getSkinFile('js/xwiki/actionbuttons/actionButtons.css', true)));
189 + loadCSS($jsontool.serialize($xwiki.getSkinFile('js/xwiki/editors/autosave.css', true)));
159 159   // Required in case the user needs to resolve merge conflicts on save.
160 160   loadCSS($jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.css', true)));
161 161   return initActionButtons();
... ... @@ -225,20 +225,27 @@
225 225   lockFailed: function() {}
226 226   }, options);
227 227   $('#xwikicontent').addClass('loading');
259 + // Lock the document first.
228 228   return lock(currentXWikiDocument).fail(options.lockFailed)
261 + // Then load the document only if we managed to lock it.
229 229   .then(load)
263 + // Then load the editors only if we managed to load the document.
230 230   .then(edit).done(options.afterEdit).always(function() {
231 231   $('#xwikicontent').removeClass('loading');
266 + // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly.
232 232   }).then(maybeSave)
233 - .then(unlock)
234 - .then(view);
268 + // Then unlock the document both when the edit ended with success and with a failure.
269 + .then(unlock, unlock)
270 + // Finally view the document both when the edit ended with success and with a failure.
271 + .then(view, view);
235 235   };
236 236  
237 237   var lock = function(xwikiDocument) {
238 - return xwikiDocument.lock().then(null, function(response) {
239 - var confirmation = response.responseJSON;
275 + return xwikiDocument.lock().then(null, function(xwikiDocument) {
240 240   // If the document was already locked then we need to ask the user if they want to force the lock.
241 - if (response.status === 423 && confirmation) {
277 + if (xwikiDocument.lockConfirmation) {
278 + var confirmation = xwikiDocument.lockConfirmation;
279 + delete xwikiDocument.lockConfirmation;
242 242   return maybeForceLock(confirmation).then($.proxy(xwikiDocument, 'lock', 'edit', true));
243 243   } else {
244 244   new XWiki.widgets.Notification(
... ... @@ -245,6 +245,7 @@
245 245   $jsontool.serialize($services.localization.render('edit.inplace.page.lockFailed')),
246 246   'error'
247 247   );
286 + return xwikiDocument;
248 248   }
249 249   });
250 250   };
... ... @@ -368,24 +368,18 @@
368 368   };
369 369  
370 370   var save = function(data) {
371 - // Push the changes to the server then render the document for view. We need the view HTML both if we stop editing
372 - // now and if we continue but cancel the edit later.
373 - return push(data.document).then($.proxy(render, null, true)).then(function(xwikiDocument) {
410 + // Push the changes to the server.
411 + return push(data.document).then(function(xwikiDocument) {
374 374   // Save succeeded.
375 - if (data['continue']) {
376 - // Update the original version in order to be able to restore it on cancel.
377 - delete xwikiDocument.originalDocument;
378 - xwikiDocument.originalDocument = $.extend(true, {}, xwikiDocument);
379 - // Continue editing.
380 - return maybeSave(xwikiDocument);
381 - } else {
382 - // This is the final version.
383 - return xwikiDocument;
384 - }
385 - }, function(xwikiDocument) {
386 - // Save failed. Continue editing.
387 - return maybeSave(xwikiDocument);
388 - });
413 + return shouldReload(xwikiDocument).then(
414 + // The document was saved with merge and thus if we want to continue eding we need to reload the editor (because
415 + // its content doesn't match the saved content).
416 + reload,
417 + // No need to reload the editor because either the action was Save & View or there was no merge on save.
418 + $.proxy(maybeContinueEditing, null, data['continue'])
419 + );
420 + // Save failed. Continue editing because we may have unsaved content.
421 + }, maybeSave);
389 389   };
390 390  
391 391   var push = function(xwikiDocument) {
... ... @@ -398,9 +398,33 @@
398 398   // effect.
399 399   $(document).one('xwiki:document:saved', $.proxy(deferred, 'resolve', xwikiDocument));
400 400   $(document).one('xwiki:document:saveFailed', $.proxy(deferred, 'reject', xwikiDocument));
401 - return deferred.promise().then($.proxy(xwikiDocument, 'reload'));
434 + return deferred.promise();
402 402   };
403 403  
437 + var maybeContinueEditing = function(continueEditing, xwikiDocument) {
438 + var afterReloadAndRender = function(success, xwikiDocument) {
439 + if (continueEditing) {
440 + if (success) {
441 + // Update the original version in order to be able to restore it on cancel.
442 + delete xwikiDocument.originalDocument;
443 + xwikiDocument.originalDocument = $.extend(true, {}, xwikiDocument);
444 + }
445 + // Continue editing.
446 + return maybeSave(xwikiDocument);
447 + } else {
448 + // This is the final version. We stop editing even if the reload / render failed.
449 + return xwikiDocument;
450 + }
451 + };
452 +
453 + // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML
454 + // both if we stop editing now and if we continue but cancel the edit later.
455 + return xwikiDocument.reload().then($.proxy(render, null, true)).then(
456 + $.proxy(afterReloadAndRender, null, /* success: */ true),
457 + $.proxy(afterReloadAndRender, null, /* success: */ false)
458 + );
459 + };
460 +
404 404   var cancel = function(xwikiDocument) {
405 405   // Simply return the original version to be restored.
406 406   return xwikiDocument.originalDocument;
... ... @@ -418,6 +418,25 @@
418 418   // Make sure we unlock the document when the user navigates to another page.
419 419   $(window).on('unload pagehide', $.proxy(unlock, null, currentXWikiDocument));
420 420  
478 + var shouldReload = function(xwikiDocument) {
479 + var reloadEventFired = false;
480 + $(document).one('xwiki:actions:reload.maybe', function() {
481 + reloadEventFired = true;
482 + });
483 + var deferred = $.Deferred();
484 + // Wait a bit to see if the reload event is fired.
485 + setTimeout(function() {
486 + // Remove the listener in case the reload event wasn't fired.
487 + $(document).off('xwiki:actions:reload.maybe');
488 + if (reloadEventFired) {
489 + deferred.resolve(xwikiDocument);
490 + } else {
491 + deferred.reject(xwikiDocument);
492 + }
493 + }, 0);
494 + return deferred.promise();
495 + };
496 +
421 421   var reload = function(xwikiDocument) {
422 422   // Leave the edit mode and then re-enter.
423 423   return view(xwikiDocument, true).then(editInPlace);
... ... @@ -504,8 +504,12 @@
504 504  
505 505   var loadActionButtons = function(actionButtons) {
506 506   $(document).on('xwiki:actions:view', function() {
507 - // Hide the action buttons and disable the shortcut keys (by disabling the buttons).
508 - actionButtons.hide().find(':input').prop('disabled', true);
583 + // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit
584 + // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing
585 + // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action
586 + // buttons to have them ready for the next editing session (the user can save or cancel and then edit again
587 + // without reloading the page).
588 + actionButtons.find(':input').blur().prop('disabled', true).end().hide();
509 509   // Hide the translate button because it can be used only in edit mode for the moment.
510 510   $('#tmTranslate').addClass('hidden');
511 511   });
... ... @@ -520,9 +520,13 @@
520 520   // We need a place where actionButtons.js can add more hidden inputs.
521 521   actionButtons.append('<div class="hidden extra"/>');
522 522   var deferred = $.Deferred();
523 - require(['actionButtons'], function() {
603 + require(['actionButtons', 'xwiki-diff', 'autoSave'], function() {
524 524   overrideEditActions();
525 525   overrideAjaxSaveAndContinue();
606 + // Activate the auto-save feature passing our fake edit form. Note that autosave.js also creates an instance of
607 + // AutoSave but it doesn't do anything because it doesn't find a real edit form in the page. This is why we have
608 + // to create our own instance of AutoSave passing the right (fake) form.
609 + new XWiki.editors.AutoSave({form: fakeForm});
526 526   var xwikiDocument = actionButtons.data('xwikiDocument');
527 527   // Enable the action buttons (and their shortcut keys) only if we're editing a document.
528 528   actionButtons.find(':input').prop('disabled', !xwikiDocument);
... ... @@ -559,6 +559,9 @@
559 559   insert: function(element) {
560 560   this._getActionButtons().find('.hidden.extra').append(element);
561 561   },
646 + down: function(selector) {
647 + return this._getActionButtons().find(selector)[0];
648 + },
562 562   serialize: function() {
563 563   var extra = this._getActionButtons().find(':input').serializeArray().reduce(function(extra, entry) {
564 564   var value = extra[entry.name] || [];
... ... @@ -568,13 +568,21 @@
568 568   }, {});
569 569   var xwikiDocument = this._getActionButtons().data('xwikiDocument');
570 570   var formData = {
571 - title: xwikiDocument.title,
572 - content: xwikiDocument.renderedContent,
573 - RequiresHTMLConversion: 'content',
574 - content_syntax: xwikiDocument.syntax,
658 + title: xwikiDocument.rawTitle,
575 575   language: xwikiDocument.getRealLocale(),
576 576   isNew: xwikiDocument.isNew
577 577   };
662 + if (xwikiDocument.content != xwikiDocument.originalDocument.content) {
663 + // Submit the raw (source) content. No syntax conversion is needed in this case.
664 + formData.content = xwikiDocument.content;
665 + } else {
666 + // Submit the rendered content (HTML), but make sure it is converted to the document syntax on the server.
667 + $.extend(formData, {
668 + content: xwikiDocument.renderedContent,
669 + RequiresHTMLConversion: 'content',
670 + content_syntax: xwikiDocument.syntax
671 + });
672 + }
578 578   // Check for merge conflicts only if the document is not new and we know the current version.
579 579   if (!xwikiDocument.isNew && xwikiDocument.version) {
580 580   formData.previousVersion = xwikiDocument.version;
... ... @@ -609,11 +609,14 @@
609 609   return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments);
610 610   }
611 611   },
612 - maybeRedirect: function() {
707 + maybeRedirect: function(continueEditing) {
613 613   if ($('.inplace-editing-buttons').is(':visible')) {
614 - // Never redirect when leaving the edit mode because we're already in view mode.
615 - return false;
709 + // Overwrite the default behavior so that we don't redirect when leaving the edit mode because we're already
710 + // in view mode. We still need to report a redirect (return true) if we don't continue editing, so that
711 + // actionButtons.js behaves as if a redirect was done.
712 + return !continueEditing;
616 616   } else {
714 + // Fallback on the default behavior if the in-place editing buttons are hidden.
617 617   return originalAjaxSaveAndContinue.maybeRedirect.apply(this, arguments);
618 618   }
619 619   }
... ... @@ -623,7 +623,7 @@
623 623   var initTitleEditor = function(xwikiDocument) {
624 624   var label = $('<label for="document-title-input" class="sr-only"/>')
625 625   .text($jsontool.serialize($services.localization.render('core.editors.content.titleField.label')));
626 - var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.title);
724 + var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.rawTitle);
627 627   var placeholder = xwikiDocument.documentReference.name;
628 628   if (placeholder === 'WebHome') {
629 629   placeholder = xwikiDocument.documentReference.parent.name;
... ... @@ -631,12 +631,12 @@
631 631   input.attr('placeholder', placeholder);
632 632   $('#document-title h1').addClass('editable').empty().append([label, input]);
633 633   $(document).on('xwiki:actions:beforeSave.titleEditor', function(event) {
634 - xwikiDocument.title = input.val();
732 + xwikiDocument.rawTitle = input.val();
635 635   });
636 636   $(document).one('xwiki:actions:view', function(event, data) {
637 637   // Destroy the title editor.
638 638   $(document).off('xwiki:actions:beforeSave.titleEditor');
639 - $('#document-title h1').removeClass('editable').text(xwikiDocument.title);
737 + $('#document-title h1').removeClass('editable').text(xwikiDocument.rawTitle);
640 640   });
641 641   return xwikiDocument;
642 642   };
... ... @@ -658,7 +658,11 @@
658 658   editMode: 'wysiwyg',
659 659   document: xwikiDocument,
660 660   // The content editor is loaded on demand, asynchronously.
661 - deferred: $.Deferred()
759 + deferred: $.Deferred(),
760 + // We have to explicitly enable the source mode for in-line edit because the latest version of the content editor
761 + // could be installed on an older version of XWiki where the in-place editor didn't support the source mode (so
762 + // the content editor cannot enable the source mode by default).
763 + enableSourceMode: true
662 662   };
663 663   var editContentPromise = data.deferred.promise();
664 664   editContentPromise.done(function() {
... ... @@ -696,6 +696,11 @@
696 696  });
697 697  
698 698  require(['jquery'], function($) {
801 + // We can edit in-place only if the #xwikicontent element is present.
802 + if (!$('#xwikicontent').length) {
803 + return;
804 + }
805 +
699 699   var inplaceEditingConfig = $('div[data-inplace-editing-config]').data('inplaceEditingConfig') || {};
700 700   var wysiwygEditorModule = 'xwiki-' + inplaceEditingConfig.wysiwygEditor + '-inline';
701 701  
XWiki.StyleSheetExtension[0]
Code
... ... @@ -6,6 +6,7 @@
6 6   margin-bottom: @line-height-computed / 4;
7 7  }
8 8  
9 +@document-title-input-padding-vertical: @line-height-computed / 4 - 1;
9 9  input#document-title-input {
10 10   /* Preserve the heading styles. */
11 11   border: 1px solid transparent;
... ... @@ -12,9 +12,10 @@
12 12   box-shadow: none;
13 13   color: inherit;
14 14   font-size: inherit;
15 - height: auto;
16 + /* It seems it's not enough to set the line height for the text input. We also need to set its height. */
17 + height: @font-size-document-title * @headings-line-height + 2 * (1 + @document-title-input-padding-vertical);
16 16   line-height: @headings-line-height;
17 - padding: (@line-height-computed / 4 - 1) (ceil(@grid-gutter-width / 2) - 1);
19 + padding: @document-title-input-padding-vertical (ceil(@grid-gutter-width / 2) - 1);
18 18   width: 100%;
19 19  }
20 20  
... ... @@ -47,3 +47,8 @@
47 47  #xwikicontent {
48 48   padding-top: @line-height-computed * 0.75;
49 49  }
52 +
53 +.sticky-buttons-wrapper {
54 + /* Leave some space for the bottom box shadow of the editing area. */
55 + margin-top: 7px;
56 +}
XWiki.UIExtensionClass[0]
Executed Content
... ... @@ -1,6 +1,6 @@
1 1  {{velocity}}
2 2  {{html clean="false"}}
3 -#if ($xcontext.action == 'view' && !$doc.isNew())
3 +#if ($services.edit.document.inPlaceEditingEnabled() && $hasEdit && $xcontext.action == 'view' && !$doc.isNew())
4 4   ## We support in-place editing only for the WYSIWYG edit mode ATM.
5 5   #getDefaultDocumentEditor($defaultEditMode)
6 6   #if ($defaultEditMode == 'wysiwyg')
XWiki.UIExtensionClass[1]
Executed Content
... ... @@ -5,8 +5,8 @@
5 5  ## * we're loading the original document version
6 6  ## * the original document version has a locale specified (it doesn't make sense to translate technical documents)
7 7  ## * the current UI locale doesn't match the original document locale
8 -#if ($xwiki.isMultiLingual() && $tdoc.realLocale == $doc.realLocale && "$!doc.realLocale" != ''
9 - && $doc.realLocale != $xcontext.locale)
8 +#if ($services.edit.document.inPlaceEditingEnabled() && $hasEdit && $xwiki.isMultiLingual()
9 + && $tdoc.realLocale == $doc.realLocale && "$!doc.realLocale" != '' && $doc.realLocale != $xcontext.locale)
10 10   #set ($url = $doc.getURL('edit', $escapetool.url({'language': $xcontext.locale})))
11 11   #set ($hint = $services.localization.render('edit.inplace.page.translate.hint',
12 12   [$xcontext.locale.getDisplayName($xcontext.locale)]))