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

Summary

Details

XWiki.JavaScriptExtension[0]
Code
... ... @@ -87,24 +87,26 @@
87 87   timestamp: new Date().getTime()
88 88   };
89 89   if (!forView) {
90 - // We need the annotated HTML when editing in order to be able to protect the rendering transformations and to
90 + // We need the annotated XHTML 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 = 'annotatedhtml';
93 - queryString.outputSyntaxVersion = '5.0'
92 + queryString.outputSyntax = 'annotatedxhtml';
94 94   // Currently, only the macro transformations are protected and thus can be edited.
95 95   // See XRENDERING-78: Add markers to modified XDOM by Transformations/Macros
96 96   queryString.transformations = 'macro';
97 97   }
98 - return Promise.resolve($.get(this.getURL('view'), queryString)).then(html => {
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) {
99 99   // Render succeeded.
100 100   var container = $('<div/>').html(html);
101 - return $.extend(this, {
103 + return $.extend(thisXWikiDocument, {
102 102   renderedTitle: container.find('#document-title h1').html(),
103 103   renderedContent: container.find('#xwikicontent').html()
104 104   });
105 - }).catch(() => {
106 - new XWiki.widgets.Notification(l10n['edit.inplace.page.renderFailed'], 'error');
107 - return Promise.reject(this);
107 + }, function() {
108 + // Render failed.
109 + return thisXWikiDocument;
108 108   });
109 109   },
110 110  
... ... @@ -114,19 +114,20 @@
114 114   * @return a promise that resolves to this document instance if the reload request succeeds
115 115   */
116 116   reload: function() {
117 - return Promise.resolve($.getJSON(this.getRestURL(), {
119 + var thisXWikiDocument = this;
120 + return $.getJSON(this.getRestURL(), {
118 118   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
119 119   timestamp: new Date().getTime()
120 - })).then(newXWikiDocument => {
123 + }).then(function(newXWikiDocument) {
121 121   // Reload succeeded.
122 122   // Resolve the document reference.
123 - this.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);
126 + thisXWikiDocument.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);
124 124   // We were able to load the document so it's not new.
125 - this.isNew = false;
126 - return $.extend(this, newXWikiDocument);
127 - }).catch(() => {
128 + thisXWikiDocument.isNew = false;
129 + return $.extend(thisXWikiDocument, newXWikiDocument);
130 + }, function() {
128 128   // Reload failed.
129 - return Promise.reject(this);
132 + return thisXWikiDocument;
130 130   });
131 131   },
132 132  
... ... @@ -137,8 +137,9 @@
137 137   * @return a promise that resolves to this document instance if the lock request succeeds
138 138   */
139 139   lock: function(action, force) {
143 + var thisXWikiDocument = this;
140 140   action = action || 'edit';
141 - return Promise.resolve($.getJSON(this.getURL('get'), {
145 + return $.getJSON(this.getURL('get'), {
142 142   sheet: 'XWiki.InplaceEditing',
143 143   action: 'lock',
144 144   lockAction: action,
... ... @@ -147,20 +147,20 @@
147 147   outputSyntax: 'plain',
148 148   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
149 149   timestamp: new Date().getTime()
150 - })).then(() => {
154 + }).then(function() {
151 151   // Lock succeeded.
152 - this.locked = action;
153 - return this;
154 - }).catch(response => {
156 + thisXWikiDocument.locked = action;
157 + return thisXWikiDocument;
158 + }, function(response) {
155 155   // Lock failed.
156 - delete this.locked;
160 + delete thisXWikiDocument.locked;
157 157   // Check if the user can force the lock.
158 158   var lockConfirmation = response.responseJSON;
159 159   if (response.status === 423 && lockConfirmation) {
160 160   // The user can force the lock, but needs confirmation.
161 - this.lockConfirmation = lockConfirmation;
165 + thisXWikiDocument.lockConfirmation = lockConfirmation;
162 162   }
163 - return Promise.reject(this);
167 + return thisXWikiDocument;
164 164   });
165 165   },
166 166  
... ... @@ -223,7 +223,7 @@
223 223   };
224 224  
225 225   var loadCSS = function(url) {
226 - $('<link/>').attr({
230 + var link = $('<link>').attr({
227 227   type: 'text/css',
228 228   rel: 'stylesheet',
229 229   href: url
... ... @@ -255,7 +255,7 @@
255 255   $('#xwikicontent').removeAttr('tabindex');
256 256   if (sectionId) {
257 257   // Select the heading of the specified section.
258 - $('#xwikicontent > #' + $.escapeSelector(sectionId)).each(function() {
262 + $('#xwikicontent > #' + escapeSelector(sectionId)).each(function() {
259 259   selectText(this);
260 260   });
261 261   }
... ... @@ -263,6 +263,22 @@
263 263   });
264 264   };
265 265  
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 +
266 266   // We preserve the document data between edits in order to be able to know which document translation should be edited
267 267   // (e.g. when the document translation is missing and we create it, the next edit session should target the created
268 268   // translation).
... ... @@ -270,11 +270,6 @@
270 270   language: xcontext.locale
271 271   }, xwikiDocumentAPI);
272 272  
273 - var setCurrentXWikiDocument = function(xwikiDocument) {
274 - currentXWikiDocument = xwikiDocument;
275 - return Promise.resolve(xwikiDocument);
276 - };
277 -
278 278   var editInPlace = function(options) {
279 279   options = $.extend({
280 280   afterEdit: function() {},
... ... @@ -282,18 +282,12 @@
282 282   }, options);
283 283   $('#xwikicontent').addClass('loading');
284 284   // Lock the document first.
285 - return lock(currentXWikiDocument)
300 + return lock(currentXWikiDocument).fail(options.lockFailed)
286 286   // Then load the document only if we managed to lock it.
287 - .then(load, xwikiDocument => {
288 - options.lockFailed(xwikiDocument);
289 - return Promise.reject(xwikiDocument);
302 + .then(load)
290 290   // Then load the editors only if we managed to load the document.
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');
304 + .then(edit).done(options.afterEdit).always(function() {
305 + $('#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.
... ... @@ -300,39 +300,34 @@
300 300   .then(unlock, unlock)
301 301   // Finally view the document both when the edit ended with success and with a failure.
302 302   .then(view, view)
303 - // Update the current document for the next edit session.
304 - .then(setCurrentXWikiDocument, setCurrentXWikiDocument);
312 + .always(function(xwikiDocument) {
313 + // Update the current document for the next edit session.
314 + currentXWikiDocument = xwikiDocument;
315 + });
305 305   };
306 306  
307 307   var lock = function(xwikiDocument) {
308 - return xwikiDocument.lock().catch(function(xwikiDocument) {
319 + return xwikiDocument.lock().then(null, function(xwikiDocument) {
309 309   // If the document was already locked then we need to ask the user if they want to force the lock.
310 310   if (xwikiDocument.lockConfirmation) {
311 311   var confirmation = xwikiDocument.lockConfirmation;
312 312   delete xwikiDocument.lockConfirmation;
313 - return maybeForceLock(confirmation).then(xwikiDocument.lock.bind(xwikiDocument, 'edit', true), function() {
324 + return maybeForceLock(confirmation).then($.proxy(xwikiDocument, 'lock', 'edit', true), function() {
314 314   // Cancel the edit action.
315 - return Promise.reject(xwikiDocument);
326 + return xwikiDocument;
316 316   });
317 317   } else {
318 318   new XWiki.widgets.Notification(l10n['edit.inplace.page.lockFailed'], 'error');
319 - return Promise.reject(xwikiDocument);
330 + return xwikiDocument;
320 320   }
321 321   });
322 322   };
323 323  
324 324   var maybeForceLock = function(confirmation) {
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 - });
336 + var deferred = $.Deferred();
333 333   // Reuse the confirmation modal once it is created.
334 334   var modal = $('.force-edit-lock-modal');
335 - if (!modal.length) {
339 + if (modal.length === 0) {
336 336   modal = createForceLockModal();
337 337   }
338 338   // Update the deferred that needs to be resolved or rejected.
... ... @@ -348,7 +348,7 @@
348 348   }
349 349   // Show the confirmation modal.
350 350   modal.modal('show');
351 - return promise;
355 + return deferred.promise();
352 352   };
353 353  
354 354   var createForceLockModal = function() {
... ... @@ -372,16 +372,16 @@
372 372   '</div>'
373 373   ].join(''));
374 374   modal.find('.close').attr('aria-label', l10n['edit.inplace.close']);
375 - modal.find('.modal-footer .btn-warning').on('click', function() {
379 + modal.find('.modal-footer .btn-warning').click(function() {
376 376   // The user has confirmed they want to force the lock.
377 377   modal.data('deferred').resolve();
378 378   modal.modal('hide');
379 379   });
380 380   modal.on('hide.bs.modal', function() {
381 - // If the lock promise is not yet settled when the modal is closing then it means the modal was canceled,
385 + // If the lock promise is not yet resolved when the modal is closing then it means the modal was canceled,
382 382   // i.e. the user doesn't want to force the lock.
383 383   var deferred = modal.data('deferred');
384 - if (!deferred.settled) {
388 + if (deferred.state() === 'pending') {
385 385   deferred.reject();
386 386   }
387 387   });
... ... @@ -389,19 +389,17 @@
389 389   };
390 390  
391 391   var load = function(xwikiDocument) {
392 - return xwikiDocument.reload().then(xwikiDocument => {
396 + return xwikiDocument.reload().done(function(xwikiDocument) {
393 393   // Clone the current document version and keep a reference to it in order to be able to restore it on cancel.
394 394   xwikiDocument.originalDocument = $.extend(true, {
395 395   renderedTitle: $('#document-title h1').html(),
396 396   renderedContent: $('#xwikicontent').html()
397 397   }, xwikiDocument);
398 - return xwikiDocument;
399 - }).catch(xwikiDocument => {
402 + }).fail(function() {
400 400   new XWiki.widgets.Notification(l10n['edit.inplace.page.loadFailed'], 'error');
401 - return Promise.reject(xwikiDocument);
402 402   // Render the document for edit, in order to have the annotated content HTML. The annotations are used to protect
403 403   // the rendering transformations (e.g. macros) when editing the content.
404 - }).then(render.bind(null, false));
406 + }).then($.proxy(render, null, false));
405 405   };
406 406  
407 407   /**
... ... @@ -414,7 +414,7 @@
414 414   };
415 415  
416 416   var maybeSave = function(xwikiDocument) {
417 - return waitForAction(xwikiDocument).then(action => {
419 + return waitForAction(xwikiDocument).then(function(action) {
418 418   switch(action.name) {
419 419   case 'save': return save({
420 420   document: action.document,
... ... @@ -427,29 +427,29 @@
427 427   };
428 428  
429 429   var waitForAction = function(xwikiDocument) {
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 - });
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
446 446   });
447 447   });
449 + return deferred.promise();
448 448   };
449 449  
450 450   var save = function(data) {
451 451   // Push the changes to the server.
452 - return push(data.document).then(xwikiDocument => {
454 + return push(data.document).then(function(xwikiDocument) {
453 453   // Save succeeded.
454 454   return shouldReload(xwikiDocument).then(
455 455   // The document was saved with merge and thus if we want to continue eding we need to reload the editor (because
... ... @@ -456,7 +456,7 @@
456 456   // its content doesn't match the saved content).
457 457   reload,
458 458   // No need to reload the editor because either the action was Save & View or there was no merge on save.
459 - maybeContinueEditing.bind(null, data['continue'])
461 + $.proxy(maybeContinueEditing, null, data['continue'])
460 460   );
461 461   // Save failed. Continue editing because we may have unsaved content.
462 462   }, maybeSave);
... ... @@ -464,15 +464,15 @@
464 464  
465 465   var push = function(xwikiDocument) {
466 466   // Let actionButtons.js do the push. We just catch the result.
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 - });
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();
476 476   };
477 477  
478 478   var maybeContinueEditing = function(continueEditing, xwikiDocument) {
... ... @@ -493,9 +493,9 @@
493 493  
494 494   // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML
495 495   // both if we stop editing now and if we continue but cancel the edit later.
496 - return xwikiDocument.reload().then(render.bind(null, true)).then(
497 - afterReloadAndRender.bind(null, /* success: */ true),
498 - afterReloadAndRender.bind(null, /* success: */ false)
498 + return xwikiDocument.reload().then($.proxy(render, null, true)).then(
499 + $.proxy(afterReloadAndRender, null, /* success: */ true),
500 + $.proxy(afterReloadAndRender, null, /* success: */ false)
499 499   );
500 500   };
501 501  
... ... @@ -514,7 +514,7 @@
514 514   };
515 515  
516 516   // Make sure we unlock the document when the user navigates to another page.
517 - $(window).on('unload pagehide', unlock.bind(null, currentXWikiDocument));
519 + $(window).on('unload pagehide', $.proxy(unlock, null, currentXWikiDocument));
518 518  
519 519   var shouldReload = function(xwikiDocument) {
520 520   var reloadEventFired = false;
... ... @@ -521,18 +521,18 @@
521 521   $(document).one('xwiki:actions:reload.maybe', '.xcontent.form', function() {
522 522   reloadEventFired = true;
523 523   });
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 - });
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();
536 536   };
537 537  
538 538   var reload = function(xwikiDocument) {
... ... @@ -559,7 +559,7 @@
559 559   if (window.location.hash === '#edit' || window.location.hash === '#translate') {
560 560   history.replaceState(null, null, '#');
561 561   }
562 - return Promise.resolve(xwikiDocument);
564 + return $.Deferred().resolve(xwikiDocument).promise();
563 563   };
564 564  
565 565   var edit = function(xwikiDocument) {
... ... @@ -581,43 +581,30 @@
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);
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);
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')
599 + 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();
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();
619 619   }
620 - return Promise.resolve(xwikiDocument);
609 + return $.Deferred().resolve(xwikiDocument).promise();
621 621   }
622 622   };
623 623  
... ... @@ -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();
655 + 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();
... ... @@ -689,52 +689,114 @@
689 689   .parent().removeClass('hidden');
690 690   }
691 691   });
692 - return Promise.resolve($.get(XWiki.currentDocument.getURL('get'), {
670 + return $.get(XWiki.currentDocument.getURL('get'), {
693 693   xpage: 'editactions'
694 - })).then(html => {
672 + }).then(function(html) {
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');
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"/>');
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 - 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 - });
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);
708 708   });
709 - }).catch(() => {
695 + return deferred.promise();
696 + }, function() {
710 710   new XWiki.widgets.Notification(l10n['edit.inplace.actionButtons.loadFailed'], 'error');
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());
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);
735 735   }
736 736   };
737 737  
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 +
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);
784 + 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);
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;
785 785   }
811 + 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();
... ... @@ -813,7 +813,7 @@
813 813   deferred: $.Deferred()
814 814   });
815 815   editContent.trigger('xwiki:actions:edit', data);
816 - return data.deferred.promise().then(() => {
842 + return data.deferred.done(function() {
817 817   editContent.show();
818 818   viewContent.remove();
819 819   if (withFocus) {
... ... @@ -824,8 +824,7 @@
824 824   editContent[0].focus({preventScroll: true});
825 825   }, 0);
826 826   }
827 - return xwikiDocument;
828 - });
853 + }).promise();
829 829   };
830 830  
831 831   var startRealTimeEditingSession = function(xwikiDocument) {
... ... @@ -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;
... ... @@ -894,17 +894,14 @@
894 894   require(['editInPlace', wysiwygEditorModule], function(editInPlace) {
895 895   // Re-enable the translate button because it can be used while editing to create the missing translation.
896 896   translateButton.removeClass('disabled');
897 - handler.edit(editInPlace, data).finally(function() {
918 + handler.edit(editInPlace, data).always(function() {
898 898   // Restore only the edit button at the end because:
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 - }, disableInPlaceEditing.bind(event.target));
925 + }, $.proxy(disableInPlaceEditing, event.target));
908 908   };
909 909  
910 910   var disableInPlaceEditing = function() {
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)]
... ... @@ -49,6 +49,7 @@
49 49   'editButtonSelector': '#tmEdit > a',
50 50   'translateButtonSelector': '#tmTranslate > a',
51 51   '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,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'))