Changes for page InplaceEditing
From 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]
To 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]
Summary
-
Objects (3 modified, 0 added, 0 removed)
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 - newXWiki.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 - r eturnnew Promise((resolve,reject)=>{468 - 469 - 470 - 471 - remaining event won't be able to change that. The remaining event listener could be called later but it won't472 - haveany effect.473 - .bind(null, xwikiDocument));474 - .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 - r eturnnew Promise((resolve,reject)=>{525 - 526 - 527 - 528 - 529 - 530 - 531 - 532 - 533 - 534 - 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'))