Changes for page InplaceEditing

From version 3.1
edited by Nazzareno Pompei
on 22/12/2021 09:31
Change comment: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/13.10.1]
To 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]

Summary

Details

XWiki.JavaScriptExtension[0]
Code
... ... @@ -87,26 +87,24 @@
87 87   timestamp: new Date().getTime()
88 88   };
89 89   if (!forView) {
90 - // We need the annotated XHTML when editing in order to be able to protect the rendering transformations and to
90 + // We need the annotated HTML when editing in order to be able to protect the rendering transformations and to
91 91   // be able to recreate the wiki syntax.
92 - queryString.outputSyntax = 'annotatedxhtml';
92 + queryString.outputSyntax = 'annotatedhtml';
93 + queryString.outputSyntaxVersion = '5.0'
93 93   // Currently, only the macro transformations are protected and thus can be edited.
94 94   // See XRENDERING-78: Add markers to modified XDOM by Transformations/Macros
95 95   queryString.transformations = 'macro';
96 96   }
97 - var thisXWikiDocument = this;
98 - return $.get(this.getURL('view'), queryString).fail(function() {
99 - new XWiki.widgets.Notification(l10n['edit.inplace.page.renderFailed'], 'error');
100 - }).then(function(html) {
98 + return Promise.resolve($.get(this.getURL('view'), queryString)).then(html => {
101 101   // Render succeeded.
102 102   var container = $('<div/>').html(html);
103 - return $.extend(thisXWikiDocument, {
101 + return $.extend(this, {
104 104   renderedTitle: container.find('#document-title h1').html(),
105 105   renderedContent: container.find('#xwikicontent').html()
106 106   });
107 - }, function() {
108 - // Render failed.
109 - return thisXWikiDocument;
105 + }).catch(() => {
106 + new XWiki.widgets.Notification(l10n['edit.inplace.page.renderFailed'], 'error');
107 + return Promise.reject(this);
110 110   });
111 111   },
112 112  
... ... @@ -116,20 +116,19 @@
116 116   * @return a promise that resolves to this document instance if the reload request succeeds
117 117   */
118 118   reload: function() {
119 - var thisXWikiDocument = this;
120 - return $.getJSON(this.getRestURL(), {
117 + return Promise.resolve($.getJSON(this.getRestURL(), {
121 121   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
122 122   timestamp: new Date().getTime()
123 - }).then(function(newXWikiDocument) {
120 + })).then(newXWikiDocument => {
124 124   // Reload succeeded.
125 125   // Resolve the document reference.
126 - thisXWikiDocument.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);
123 + this.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);
127 127   // We were able to load the document so it's not new.
128 - thisXWikiDocument.isNew = false;
129 - return $.extend(thisXWikiDocument, newXWikiDocument);
130 - }, function() {
125 + this.isNew = false;
126 + return $.extend(this, newXWikiDocument);
127 + }).catch(() => {
131 131   // Reload failed.
132 - return thisXWikiDocument;
129 + return Promise.reject(this);
133 133   });
134 134   },
135 135  
... ... @@ -140,9 +140,8 @@
140 140   * @return a promise that resolves to this document instance if the lock request succeeds
141 141   */
142 142   lock: function(action, force) {
143 - var thisXWikiDocument = this;
144 144   action = action || 'edit';
145 - return $.getJSON(this.getURL('get'), {
141 + return Promise.resolve($.getJSON(this.getURL('get'), {
146 146   sheet: 'XWiki.InplaceEditing',
147 147   action: 'lock',
148 148   lockAction: action,
... ... @@ -151,20 +151,20 @@
151 151   outputSyntax: 'plain',
152 152   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
153 153   timestamp: new Date().getTime()
154 - }).then(function() {
150 + })).then(() => {
155 155   // Lock succeeded.
156 - thisXWikiDocument.locked = action;
157 - return thisXWikiDocument;
158 - }, function(response) {
152 + this.locked = action;
153 + return this;
154 + }).catch(response => {
159 159   // Lock failed.
160 - delete thisXWikiDocument.locked;
156 + delete this.locked;
161 161   // Check if the user can force the lock.
162 162   var lockConfirmation = response.responseJSON;
163 163   if (response.status === 423 && lockConfirmation) {
164 164   // The user can force the lock, but needs confirmation.
165 - thisXWikiDocument.lockConfirmation = lockConfirmation;
161 + this.lockConfirmation = lockConfirmation;
166 166   }
167 - return thisXWikiDocument;
163 + return Promise.reject(this);
168 168   });
169 169   },
170 170  
... ... @@ -227,7 +227,7 @@
227 227   };
228 228  
229 229   var loadCSS = function(url) {
230 - var link = $('<link>').attr({
226 + $('<link/>').attr({
231 231   type: 'text/css',
232 232   rel: 'stylesheet',
233 233   href: url
... ... @@ -259,7 +259,7 @@
259 259   $('#xwikicontent').removeAttr('tabindex');
260 260   if (sectionId) {
261 261   // Select the heading of the specified section.
262 - $('#xwikicontent > #' + escapeSelector(sectionId)).each(function() {
258 + $('#xwikicontent > #' + $.escapeSelector(sectionId)).each(function() {
263 263   selectText(this);
264 264   });
265 265   }
... ... @@ -267,22 +267,6 @@
267 267   });
268 268   };
269 269  
270 - var escapeSelector = function(selector) {
271 - if (window.CSS && typeof CSS.escape === 'function') {
272 - // Not supported by Internet Explorer.
273 - return CSS.escape(selector);
274 - } else if (typeof $.escapeSelector === 'function') {
275 - // Added in jQuery 3.0
276 - return $.escapeSelector(selector);
277 - } else if (typeof selector === 'string') {
278 - // Simple implementation.
279 - // See https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/
280 - return selector.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1');
281 - } else {
282 - return selector;
283 - }
284 - };
285 -
286 286   // We preserve the document data between edits in order to be able to know which document translation should be edited
287 287   // (e.g. when the document translation is missing and we create it, the next edit session should target the created
288 288   // translation).
... ... @@ -290,6 +290,11 @@
290 290   language: xcontext.locale
291 291   }, xwikiDocumentAPI);
292 292  
273 + var setCurrentXWikiDocument = function(xwikiDocument) {
274 + currentXWikiDocument = xwikiDocument;
275 + return Promise.resolve(xwikiDocument);
276 + };
277 +
293 293   var editInPlace = function(options) {
294 294   options = $.extend({
295 295   afterEdit: function() {},
... ... @@ -297,12 +297,18 @@
297 297   }, options);
298 298   $('#xwikicontent').addClass('loading');
299 299   // Lock the document first.
300 - return lock(currentXWikiDocument).fail(options.lockFailed)
285 + return lock(currentXWikiDocument)
301 301   // Then load the document only if we managed to lock it.
302 - .then(load)
287 + .then(load, xwikiDocument => {
288 + options.lockFailed(xwikiDocument);
289 + return Promise.reject(xwikiDocument);
303 303   // Then load the editors only if we managed to load the document.
304 - .then(edit).done(options.afterEdit).always(function() {
305 - $('#xwikicontent').removeClass('loading');
291 + }).then(edit).then(xwikiDocument => {
292 + options.afterEdit(xwikiDocument);
293 + return xwikiDocument;
294 + }).finally(() => {
295 + // Remove the aria-expanded attribute which is incorrect for role=textbox
296 + $('#xwikicontent').removeClass('loading').removeAttr('aria-expanded');
306 306   // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly.
307 307   }).then(maybeSave)
308 308   // Then unlock the document both when the edit ended with success and with a failure.
... ... @@ -309,34 +309,39 @@
309 309   .then(unlock, unlock)
310 310   // Finally view the document both when the edit ended with success and with a failure.
311 311   .then(view, view)
312 - .always(function(xwikiDocument) {
313 - // Update the current document for the next edit session.
314 - currentXWikiDocument = xwikiDocument;
315 - });
303 + // Update the current document for the next edit session.
304 + .then(setCurrentXWikiDocument, setCurrentXWikiDocument);
316 316   };
317 317  
318 318   var lock = function(xwikiDocument) {
319 - return xwikiDocument.lock().then(null, function(xwikiDocument) {
308 + return xwikiDocument.lock().catch(function(xwikiDocument) {
320 320   // If the document was already locked then we need to ask the user if they want to force the lock.
321 321   if (xwikiDocument.lockConfirmation) {
322 322   var confirmation = xwikiDocument.lockConfirmation;
323 323   delete xwikiDocument.lockConfirmation;
324 - return maybeForceLock(confirmation).then($.proxy(xwikiDocument, 'lock', 'edit', true), function() {
313 + return maybeForceLock(confirmation).then(xwikiDocument.lock.bind(xwikiDocument, 'edit', true), function() {
325 325   // Cancel the edit action.
326 - return xwikiDocument;
315 + return Promise.reject(xwikiDocument);
327 327   });
328 328   } else {
329 329   new XWiki.widgets.Notification(l10n['edit.inplace.page.lockFailed'], 'error');
330 - return xwikiDocument;
319 + return Promise.reject(xwikiDocument);
331 331   }
332 332   });
333 333   };
334 334  
335 335   var maybeForceLock = function(confirmation) {
336 - var deferred = $.Deferred();
325 + var deferred, promise = new Promise((resolve, reject) => {
326 + deferred = {resolve, reject};
327 + });
328 + // We need the catch() to prevent the "Uncaught (in promise)" error log in the console.
329 + promise.catch(() => {}).finally(() => {
330 + // This flag is used by the Force Lock modal to know whether the promise is settled when the modal is closing.
331 + deferred.settled = true;
332 + });
337 337   // Reuse the confirmation modal once it is created.
338 338   var modal = $('.force-edit-lock-modal');
339 - if (modal.length === 0) {
335 + if (!modal.length) {
340 340   modal = createForceLockModal();
341 341   }
342 342   // Update the deferred that needs to be resolved or rejected.
... ... @@ -352,7 +352,7 @@
352 352   }
353 353   // Show the confirmation modal.
354 354   modal.modal('show');
355 - return deferred.promise();
351 + return promise;
356 356   };
357 357  
358 358   var createForceLockModal = function() {
... ... @@ -376,16 +376,16 @@
376 376   '</div>'
377 377   ].join(''));
378 378   modal.find('.close').attr('aria-label', l10n['edit.inplace.close']);
379 - modal.find('.modal-footer .btn-warning').click(function() {
375 + modal.find('.modal-footer .btn-warning').on('click', function() {
380 380   // The user has confirmed they want to force the lock.
381 381   modal.data('deferred').resolve();
382 382   modal.modal('hide');
383 383   });
384 384   modal.on('hide.bs.modal', function() {
385 - // If the lock promise is not yet resolved when the modal is closing then it means the modal was canceled,
381 + // If the lock promise is not yet settled when the modal is closing then it means the modal was canceled,
386 386   // i.e. the user doesn't want to force the lock.
387 387   var deferred = modal.data('deferred');
388 - if (deferred.state() === 'pending') {
384 + if (!deferred.settled) {
389 389   deferred.reject();
390 390   }
391 391   });
... ... @@ -393,17 +393,19 @@
393 393   };
394 394  
395 395   var load = function(xwikiDocument) {
396 - return xwikiDocument.reload().done(function(xwikiDocument) {
392 + return xwikiDocument.reload().then(xwikiDocument => {
397 397   // Clone the current document version and keep a reference to it in order to be able to restore it on cancel.
398 398   xwikiDocument.originalDocument = $.extend(true, {
399 399   renderedTitle: $('#document-title h1').html(),
400 400   renderedContent: $('#xwikicontent').html()
401 401   }, xwikiDocument);
402 - }).fail(function() {
398 + return xwikiDocument;
399 + }).catch(xwikiDocument => {
403 403   new XWiki.widgets.Notification(l10n['edit.inplace.page.loadFailed'], 'error');
401 + return Promise.reject(xwikiDocument);
404 404   // Render the document for edit, in order to have the annotated content HTML. The annotations are used to protect
405 405   // the rendering transformations (e.g. macros) when editing the content.
406 - }).then($.proxy(render, null, false));
404 + }).then(render.bind(null, false));
407 407   };
408 408  
409 409   /**
... ... @@ -416,7 +416,7 @@
416 416   };
417 417  
418 418   var maybeSave = function(xwikiDocument) {
419 - return waitForAction(xwikiDocument).then(function(action) {
417 + return waitForAction(xwikiDocument).then(action => {
420 420   switch(action.name) {
421 421   case 'save': return save({
422 422   document: action.document,
... ... @@ -429,29 +429,29 @@
429 429   };
430 430  
431 431   var waitForAction = function(xwikiDocument) {
432 - var deferred = $.Deferred();
433 - // We wait for the first save, reload or cancel event, whichever is triggered first. Note that the event listeners
434 - // that are not executed first will remain registered but that doesn't cause any problems because the state of a
435 - // deferred object (promise) cannot change once it was resolved. So the first event that fires will resolve the
436 - // promise and the remaining events won't be able to change that. The remaining event listeners could be called
437 - // later but they won't have any effect on the deferred object.
438 - $(document).one([
439 - 'xwiki:actions:save',
440 - 'xwiki:actions:reload',
441 - 'xwiki:actions:cancel',
442 - ].join(' '), '.xcontent.form', function(event, data) {
443 - deferred.resolve({
444 - name: event.type.substring('xwiki:actions:'.length),
445 - document: xwikiDocument,
446 - data: data
430 + return new Promise((resolve, reject) => {
431 + // We wait for the first save, reload or cancel event, whichever is triggered first. Note that the event listeners
432 + // that are not executed first will remain registered but that doesn't cause any problems because the state of a
433 + // deferred object (promise) cannot change once it was resolved. So the first event that fires will resolve the
434 + // promise and the remaining events won't be able to change that. The remaining event listeners could be called
435 + // later but they won't have any effect on the deferred object.
436 + $(document).one([
437 + 'xwiki:actions:save',
438 + 'xwiki:actions:reload',
439 + 'xwiki:actions:cancel',
440 + ].join(' '), '.xcontent.form', function(event, data) {
441 + resolve({
442 + name: event.type.substring('xwiki:actions:'.length),
443 + document: xwikiDocument,
444 + data: data
445 + });
447 447   });
448 448   });
449 - return deferred.promise();
450 450   };
451 451  
452 452   var save = function(data) {
453 453   // Push the changes to the server.
454 - return push(data.document).then(function(xwikiDocument) {
452 + return push(data.document).then(xwikiDocument => {
455 455   // Save succeeded.
456 456   return shouldReload(xwikiDocument).then(
457 457   // The document was saved with merge and thus if we want to continue eding we need to reload the editor (because
... ... @@ -458,7 +458,7 @@
458 458   // its content doesn't match the saved content).
459 459   reload,
460 460   // No need to reload the editor because either the action was Save & View or there was no merge on save.
461 - $.proxy(maybeContinueEditing, null, data['continue'])
459 + maybeContinueEditing.bind(null, data['continue'])
462 462   );
463 463   // Save failed. Continue editing because we may have unsaved content.
464 464   }, maybeSave);
... ... @@ -466,15 +466,15 @@
466 466  
467 467   var push = function(xwikiDocument) {
468 468   // Let actionButtons.js do the push. We just catch the result.
469 - var deferred = $.Deferred();
470 - // We wait for the save request to either succeed or fail. Note that one of the event listeners will remain
471 - // registered but that doesn't cause any problems because the state of a deferred object (promise) cannot change
472 - // once it was resolved or rejected. So the first event that fires will resolve/reject the promise and the remaining
473 - // event won't be able to change that. The remaining event listener could be called later but it won't have any
474 - // effect.
475 - $(document).one('xwiki:document:saved', '.xcontent.form', $.proxy(deferred, 'resolve', xwikiDocument));
476 - $(document).one('xwiki:document:saveFailed', '.xcontent.form', $.proxy(deferred, 'reject', xwikiDocument));
477 - return deferred.promise();
467 + return new Promise((resolve, reject) => {
468 + // We wait for the save request to either succeed or fail. Note that one of the event listeners will remain
469 + // registered but that doesn't cause any problems because the state of a deferred object (promise) cannot change
470 + // once it was resolved or rejected. So the first event that fires will resolve/reject the promise and the
471 + // remaining event won't be able to change that. The remaining event listener could be called later but it won't
472 + // have any effect.
473 + $(document).one('xwiki:document:saved', '.xcontent.form', resolve.bind(null, xwikiDocument));
474 + $(document).one('xwiki:document:saveFailed', '.xcontent.form', reject.bind(null, xwikiDocument));
475 + });
478 478   };
479 479  
480 480   var maybeContinueEditing = function(continueEditing, xwikiDocument) {
... ... @@ -495,9 +495,9 @@
495 495  
496 496   // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML
497 497   // both if we stop editing now and if we continue but cancel the edit later.
498 - return xwikiDocument.reload().then($.proxy(render, null, true)).then(
499 - $.proxy(afterReloadAndRender, null, /* success: */ true),
500 - $.proxy(afterReloadAndRender, null, /* success: */ false)
496 + return xwikiDocument.reload().then(render.bind(null, true)).then(
497 + afterReloadAndRender.bind(null, /* success: */ true),
498 + afterReloadAndRender.bind(null, /* success: */ false)
501 501   );
502 502   };
503 503  
... ... @@ -516,7 +516,7 @@
516 516   };
517 517  
518 518   // Make sure we unlock the document when the user navigates to another page.
519 - $(window).on('unload pagehide', $.proxy(unlock, null, currentXWikiDocument));
517 + $(window).on('unload pagehide', unlock.bind(null, currentXWikiDocument));
520 520  
521 521   var shouldReload = function(xwikiDocument) {
522 522   var reloadEventFired = false;
... ... @@ -523,18 +523,18 @@
523 523   $(document).one('xwiki:actions:reload.maybe', '.xcontent.form', function() {
524 524   reloadEventFired = true;
525 525   });
526 - var deferred = $.Deferred();
527 - // Wait a bit to see if the reload event is fired.
528 - setTimeout(function() {
529 - // Remove the listener in case the reload event wasn't fired.
530 - $(document).off('xwiki:actions:reload.maybe');
531 - if (reloadEventFired) {
532 - deferred.resolve(xwikiDocument);
533 - } else {
534 - deferred.reject(xwikiDocument);
535 - }
536 - }, 0);
537 - return deferred.promise();
524 + return new Promise((resolve, reject) => {
525 + // Wait a bit to see if the reload event is fired.
526 + setTimeout(function() {
527 + // Remove the listener in case the reload event wasn't fired.
528 + $(document).off('xwiki:actions:reload.maybe');
529 + if (reloadEventFired) {
530 + resolve(xwikiDocument);
531 + } else {
532 + reject(xwikiDocument);
533 + }
534 + }, 0);
535 + });
538 538   };
539 539  
540 540   var reload = function(xwikiDocument) {
... ... @@ -561,7 +561,7 @@
561 561   if (window.location.hash === '#edit' || window.location.hash === '#translate') {
562 562   history.replaceState(null, null, '#');
563 563   }
564 - return $.Deferred().resolve(xwikiDocument).promise();
562 + return Promise.resolve(xwikiDocument);
565 565   };
566 566  
567 567   var edit = function(xwikiDocument) {
... ... @@ -583,30 +583,43 @@
583 583   // Thus we need to use the grid for the sticky buttons also otherwise the postion is badly computed when scrolling
584 584   // (because of the float on the previous element). This wouldn't be needed if we were using position:sticky, which
585 585   // we can't use yet because it's not implemented on IE11 which we still have to support.
586 - var actionButtonsWrapper = editContent.nextAll('.sticky-buttons-wrapper');
587 - if (actionButtonsWrapper.length === 0) {
588 - actionButtonsWrapper = $('<div class="sticky-buttons-wrapper col-xs-12">' +
589 - '<div class="inplace-editing-buttons sticky-buttons"/></div>').insertAfter(editContent).toggle(!!xwikiDocument);
590 - var actionButtons = actionButtonsWrapper.children('.sticky-buttons')
591 - .data('xwikiDocument', xwikiDocument)
592 - // Expose the fake form if an extension needs to manipulate it.
593 - .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);
594 594   return loadActionButtons(actionButtons);
595 595   } else {
596 596   // If we're editing a page..
597 597   if (xwikiDocument) {
598 598   // ..then make sure the action buttons are displayed right away (don't wait for the user to scroll).
599 - actionButtonsWrapper.show().children('.sticky-buttons')
608 + inplaceEditingForm.show().children('.sticky-buttons')
600 600   .data('xwikiDocument', xwikiDocument)
601 - // Expose the fake form if an extension needs to manipulate it.
602 - .data('fakeForm', fakeForm)
603 - // but make sure the position of the action buttons is updated.
604 - .trigger('xwiki:dom:refresh');
605 - // The action buttons are disabled on Save & View. We don't reload the page on Save & View and we reuse the
606 - // action buttons so we need to re-enable them each time we enter the edit mode.
607 - 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();
608 608   }
609 - return $.Deferred().resolve(xwikiDocument).promise();
620 + return Promise.resolve(xwikiDocument);
610 610   }
611 611   };
612 612  
... ... @@ -646,6 +646,17 @@
646 646   };
647 647  
648 648   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 + });
649 649   $(document).on('xwiki:actions:view', '.xcontent.form', function(event, data) {
650 650   // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit
651 651   // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing
... ... @@ -652,7 +652,7 @@
652 652   // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action
653 653   // buttons to have them ready for the next editing session (the user can save or cancel and then edit again
654 654   // without reloading the page).
655 - actionButtons.find(':input').blur().prop('disabled', true).end().parent().hide();
677 + actionButtons.find(':input').blur().end().prop('disabled', true).parent().hide();
656 656   // Restore the Translate button if the locale of the viewed document doesn't match the current user interface
657 657   // locale (because the viewed document doesn't have a translation in the current locale).
658 658   var xwikiDocumentLocale = data.document.getRealLocale();
... ... @@ -667,114 +667,52 @@
667 667   .parent().removeClass('hidden');
668 668   }
669 669   });
670 - return $.get(XWiki.currentDocument.getURL('get'), {
692 + return Promise.resolve($.get(XWiki.currentDocument.getURL('get'), {
671 671   xpage: 'editactions'
672 - }).then(function(html) {
694 + })).then(html => {
673 673   actionButtons.html(html);
674 674   // Fix the name of the Save & View action.
675 675   actionButtons.find('.btn-primary').first().attr('name', 'action_save');
676 - // Append the hidden input field that keeps the CSRF token.
677 - $('<input type="hidden" name="form_token" />').val(xcontext.form_token).appendTo(actionButtons);
678 - // We need a place where actionButtons.js can add more hidden inputs.
679 - actionButtons.append('<div class="hidden extra"/>');
680 680   // Let the others know that the DOM has been updated, in order to enhance it.
681 681   $(document).trigger('xwiki:dom:updated', {'elements': actionButtons.toArray()});
682 - var deferred = $.Deferred();
683 - require(['xwiki-actionButtons', 'xwiki-diff', 'xwiki-autoSave'], function() {
684 - overrideEditActions();
685 - overrideAjaxSaveAndContinue();
686 - // Activate the auto-save feature passing our fake edit form. Note that autosave.js also creates an instance of
687 - // AutoSave but it doesn't do anything because it doesn't find a real edit form in the page. This is why we have
688 - // to create our own instance of AutoSave passing the right (fake) form.
689 - new XWiki.editors.AutoSave({form: fakeForm});
690 - var xwikiDocument = actionButtons.data('xwikiDocument');
691 - // Enable the action buttons (and their shortcut keys) only if we're editing a document.
692 - actionButtons.find(':input').prop('disabled', !xwikiDocument);
693 - deferred.resolve(xwikiDocument);
700 + return new Promise((resolve, reject) => {
701 + require(['xwiki-actionButtons', 'xwiki-diff', 'xwiki-autoSave'], function() {
702 + overrideAjaxSaveAndContinue();
703 + var xwikiDocument = actionButtons.data('xwikiDocument');
704 + // Enable the action buttons (and their shortcut keys) only if we're editing a document.
705 + actionButtons.prop('disabled', !xwikiDocument);
706 + resolve(xwikiDocument);
707 + });
694 694   });
695 - return deferred.promise();
696 - }, function() {
709 + }).catch(() => {
697 697   new XWiki.widgets.Notification(l10n['edit.inplace.actionButtons.loadFailed'], 'error');
698 698   });
699 699   };
700 700  
701 - // actionButtons.js expects a form so we use a fake form. Refactoring actionButtons.js is too dangerous ATM.
702 - var fakeForm = {
703 - action: XWiki.currentDocument.getURL('save'),
704 - async: true,
705 - _getActionButtons: function() {
706 - if (!this._actionButtons) {
707 - this._actionButtons = $('#xwikicontent').nextAll('.sticky-buttons-wrapper').children('.sticky-buttons');
708 - }
709 - return this._actionButtons;
710 - },
711 - disable: function() {
712 - this._getActionButtons().find(':input').prop('disabled', true);
713 - },
714 - enable: function() {
715 - // Clear the extra hidden input fields, that actionButtons.js might have added, each time the form is (re)enabled
716 - // (i.e. after a failed Save & View or before entering the edit mode) because they are designed to be used once.
717 - this._getActionButtons().find('.hidden.extra').empty();
718 - this._getActionButtons().find(':input').prop('disabled', false);
719 - },
720 - insert: function(element) {
721 - this._getActionButtons().find('.hidden.extra').append(element);
722 - },
723 - // Note that this method only works with single argument.
724 - append: function(element) {
725 - this.insert(element);
726 - },
727 - down: function(selector) {
728 - return this._getActionButtons().find(selector)[0];
729 - },
730 - serialize: function() {
731 - var extra = this._getActionButtons().find(':input').serializeArray().reduce(function(extra, entry) {
732 - var value = extra[entry.name] || [];
733 - value.push(entry.value);
734 - extra[entry.name] = value;
735 - return extra;
736 - }, {});
737 - var xwikiDocument = this._getActionButtons().data('xwikiDocument');
738 - var formData = {
739 - title: xwikiDocument.rawTitle,
740 - language: xwikiDocument.getRealLocale(),
741 - isNew: xwikiDocument.isNew
742 - };
743 - if (xwikiDocument.content != xwikiDocument.originalDocument.content) {
744 - // Submit the raw (source) content. No syntax conversion is needed in this case.
745 - formData.content = xwikiDocument.content;
746 - } else {
747 - // Submit the rendered content (HTML), but make sure it is converted to the document syntax on the server.
748 - $.extend(formData, {
749 - content: xwikiDocument.renderedContent,
750 - RequiresHTMLConversion: 'content',
751 - content_syntax: xwikiDocument.syntax
752 - });
753 - }
754 - // Check for merge conflicts only if the document is not new and we know the current version.
755 - if (!xwikiDocument.isNew && xwikiDocument.version) {
756 - formData.previousVersion = xwikiDocument.version;
757 - formData.editingVersionDate = new Date(xwikiDocument.modified).getTime();
758 - }
759 - // Ensure that formData information has priority over extra information.
760 - 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());
761 761   }
762 762   };
763 763  
764 - var overrideEditActions = function() {
765 - // Override the EditActions.notify() function in order to pass a fake form in the event parameters.
766 - var originalNotify = XWiki.actionButtons.EditActions.prototype.notify;
767 - XWiki.actionButtons.EditActions.prototype.notify = function(originalEvent, action, params) {
768 - if (params && $(originalEvent.element()).closest('.inplace-editing-buttons').length > 0) {
769 - // actionButtons.js expects a form so we use a fake form. Refactoring actionButtons.js is too dangerous ATM.
770 - // Note that we do this only when the event has parameters because we want to exclude the cancel event for which
771 - // actionButtons.js changes the window location if a form is specified, and we want to prevent that.
772 - params.form = fakeForm;
773 - }
774 - return originalNotify.apply(this, arguments);
775 - };
776 - };
777 -
778 778   var overrideAjaxSaveAndContinue = function() {
779 779   var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype);
780 780   $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, {
... ... @@ -781,7 +781,13 @@
781 781   reloadEditor: function() {
782 782   var actionButtons = $('.inplace-editing-buttons');
783 783   if (actionButtons.is(':visible')) {
784 - 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);
785 785   } else {
786 786   return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments);
787 787   }
... ... @@ -803,12 +803,20 @@
803 803   var initTitleEditor = function(xwikiDocument) {
804 804   var label = $('<label for="document-title-input" class="sr-only"/>')
805 805   .text(l10n['core.editors.content.titleField.label']);
806 - var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.rawTitle);
807 - var placeholder = xwikiDocument.documentReference.name;
808 - if (placeholder === 'WebHome') {
809 - 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);
810 810   }
811 - input.attr('placeholder', placeholder);
812 812   $('#document-title h1').addClass('editable').empty().append([label, input]);
813 813   $(document).on('xwiki:actions:beforeSave.titleEditor', '.xcontent.form', function(event) {
814 814   xwikiDocument.rawTitle = input.val();
... ... @@ -839,7 +839,7 @@
839 839   deferred: $.Deferred()
840 840   });
841 841   editContent.trigger('xwiki:actions:edit', data);
842 - return data.deferred.done(function() {
816 + return data.deferred.promise().then(() => {
843 843   editContent.show();
844 844   viewContent.remove();
845 845   if (withFocus) {
... ... @@ -850,7 +850,8 @@
850 850   editContent[0].focus({preventScroll: true});
851 851   }, 0);
852 852   }
853 - }).promise();
827 + return xwikiDocument;
828 + });
854 854   };
855 855  
856 856   var startRealTimeEditingSession = function(xwikiDocument) {
... ... @@ -907,6 +907,10 @@
907 907   }
908 908   // Disable the edit buttons and hide the section edit links.
909 909   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');
910 910   $('#xwikicontent').children(':header').children('.edit_section').addClass('hidden');
911 911   event.preventDefault();
912 912   const handler = event.data;
... ... @@ -915,14 +915,17 @@
915 915   require(['editInPlace', wysiwygEditorModule], function(editInPlace) {
916 916   // Re-enable the translate button because it can be used while editing to create the missing translation.
917 917   translateButton.removeClass('disabled');
918 - handler.edit(editInPlace, data).always(function() {
897 + handler.edit(editInPlace, data).finally(function() {
919 919   // Restore only the edit button at the end because:
920 920   // * the translate button is restored (if needed) by the editInPlace module
921 921   // * the section edit links are restored when the document is rendered for view
922 922   editButton.removeClass('disabled');
902 + editButton.removeAttr('aria-disabled');
903 + editButton.removeAttr('role');
904 + editButton.attr('href', reference);
923 923   });
924 924   // Fallback on the standalone edit mode if we fail to load the required modules.
925 - }, $.proxy(disableInPlaceEditing, event.target));
907 + }, disableInPlaceEditing.bind(event.target));
926 926   };
927 927  
928 928   var disableInPlaceEditing = function() {
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)]
... ... @@ -48,7 +48,6 @@
48 48   'editButtonSelector': '#tmEdit > a',
49 49   'translateButtonSelector': '#tmTranslate > a',
50 50   'enableSourceMode': true,
51 - 'enableOfficeImport': $services.officemanager.isConnected(),
52 52   'paths': {
53 53   'js': {
54 54   'xwiki-actionButtons': "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.js' $jsParams)",
... ... @@ -63,6 +63,7 @@
63 63   },
64 64   'l10n': $l10n
65 65   })
66 + #set ($inplaceEditingConfig.titleIsMandatory = $xwiki.getSpacePreference('xwiki.title.mandatory') == 1)
66 66   <div class="hidden" data-inplace-editing-config="$escapetool.xml($jsontool.serialize($inplaceEditingConfig))"></div>
67 67   ## We didn't move this to the file system because it uses LESS and we didn't want to include it in the skin.
68 68   #set ($discard = $xwiki.ssx.use('XWiki.InplaceEditing'))