Changes for page InplaceEditing
From version 3.1
edited by Nazzareno Pompei
on 22/12/2021 09:31
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
on 19/01/2024 08:57
Change comment:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/15.10.5]
Summary
-
Objects (3 modified, 0 added, 0 removed)
Details
- XWiki.JavaScriptExtension[0]
-
- Code
-
... ... @@ -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 to90 + // 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(this XWikiDocument, {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 - //Renderfailed.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 - this XWikiDocument.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 - this XWikiDocument.isNew = false;129 - return $.extend(this XWikiDocument, 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 - this XWikiDocument.locked = action;157 - return this XWikiDocument;158 - } , function(response){152 + this.locked = action; 153 + return this; 154 + }).catch(response => { 159 159 // Lock failed. 160 - delete this XWikiDocument.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 - this XWikiDocument.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().th en(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.st ate() === '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 - vardeferred= $.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 remaining473 - // event won't be able to change that. The remaining event listener could be called later but it won't have any474 - // 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 - vardeferred= $.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 - acti onButtonsWrapper.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).al ways(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'))