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]
Summary
-
Page properties (2 modified, 0 added, 0 removed)
-
Objects (3 modified, 1 added, 0 removed)
Details
- Page properties
-
- Author
-
... ... @@ -1,1 +1,1 @@ 1 -XWiki.N azzarenoPompei1 +XWiki.NPompei - Content
-
... ... @@ -9,15 +9,11 @@ 9 9 #jsonResponse($editConfirmation) 10 10 #else 11 11 ## Lock the document for editing. 12 - #set ($ lockParams= {12 + #set ($discard = $response.sendRedirect($tdoc.getURL('lock', $escapetool.url({ 13 13 'ajax': 1, 14 14 'action': $request.lockAction, 15 15 'language': $tdoc.realLocale 16 - }) 17 - #if ($request.force == 'true') 18 - #set ($lockParams.force = 1) 19 - #end 20 - #set ($discard = $response.sendRedirect($tdoc.getURL('lock', $escapetool.url($lockParams)))) 16 + })))) 21 21 #end 22 22 #end 23 23 {{/velocity}}
- XWiki.JavaScriptExtension[0]
-
- Code
-
... ... @@ -1,11 +7,9 @@ 1 -(function(config) { 2 - "use strict"; 3 - 4 -const paths = config.paths; 5 -const l10n = config.l10n; 6 - 7 7 require.config({ 8 - paths: paths.js 2 + paths: { 3 + 'actionButtons': $jsontool.serialize($xwiki.getSkinFile('js/xwiki/actionbuttons/actionButtons.js', true)), 4 + // Required in case the user needs to resolve merge conflicts on save. 5 + 'diff': $jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.js')) 6 + } 9 9 }); 10 10 11 11 define('xwiki-document-api', ['jquery'], function($) { ... ... @@ -16,36 +16,13 @@ 16 16 17 17 return { 18 18 /** 19 - * @return this document's plain title 20 - */ 21 - getPlainTitle() { 22 - return $('<div/>').html(this.renderedTitle || this.title).text(); 23 - }, 24 - 25 - /** 26 26 * @return this document's real locale 27 27 */ 28 28 getRealLocale: function() { 29 - var realLocale = this.language; 30 - if (typeof realLocale !== 'string' || realLocale === '') { 31 - realLocale = this.getDefaultLocale(); 32 - } 33 - return realLocale; 20 + return this.language || (this.translations && this.translations['default']) || $('html').attr('lang'); 34 34 }, 35 35 36 36 /** 37 - * @return this document's default locale 38 - */ 39 - getDefaultLocale: function() { 40 - if (this.translations && typeof this.translations['default'] === 'string') { 41 - return this.translations['default']; 42 - } else { 43 - // The default locale is not specified. Use the UI locale. 44 - return $('html').attr('lang'); 45 - } 46 - }, 47 - 48 - /** 49 49 * @return the URL that can be used to perform the specified action on this document 50 50 */ 51 51 getURL: function(action, queryString, fragment) { ... ... @@ -80,31 +80,25 @@ 80 80 */ 81 81 render: function(forView) { 82 82 var queryString = { 83 - xpage: ' get',58 + xpage: 'rendercontent', 84 84 outputTitle: true, 60 + outputSyntax: forView ? null : 'annotatedxhtml', 85 85 language: this.getRealLocale(), 86 86 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 87 87 timestamp: new Date().getTime() 88 88 }; 89 - if (!forView) { 90 - // We need the annotated HTML when editing in order to be able to protect the rendering transformations and to 91 - // be able to recreate the wiki syntax. 92 - queryString.outputSyntax = 'annotatedhtml'; 93 - queryString.outputSyntaxVersion = '5.0' 94 - // Currently, only the macro transformations are protected and thus can be edited. 95 - // See XRENDERING-78: Add markers to modified XDOM by Transformations/Macros 96 - queryString.transformations = 'macro'; 97 - } 98 - return Promise.resolve($.get(this.getURL('view'), queryString)).then(html => { 99 - // Render succeeded. 65 + var thisXWikiDocument = this; 66 + return $.get(this.getURL('view'), queryString).fail(function() { 67 + new XWiki.widgets.Notification( 68 + $jsontool.serialize($services.localization.render('edit.inplace.page.renderFailed')), 69 + 'error' 70 + ); 71 + }).then(function(html) { 100 100 var container = $('<div/>').html(html); 101 - return $.extend(this, { 73 + 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); 108 108 }); 109 109 }, 110 110 ... ... @@ -114,19 +114,16 @@ 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(), { 86 + var thisXWikiDocument = this; 87 + 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 => { 121 - // Reload succeeded. 90 + }).then(function(newXWikiDocument) { 122 122 // Resolve the document reference. 123 - this.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT); 92 + 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 - // Reload failed. 129 - return Promise.reject(this); 94 + thisXWikiDocument.isNew = false; 95 + return $.extend(thisXWikiDocument, newXWikiDocument); 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) { 106 + var thisXWikiDocument = this; 140 140 action = action || 'edit'; 141 - return Promise.resolve($.getJSON(this.getURL('get'), {108 + return $.getJSON(this.getURL('get'), { 142 142 sheet: 'XWiki.InplaceEditing', 143 143 action: 'lock', 144 144 lockAction: action, ... ... @@ -147,20 +147,9 @@ 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(() => { 151 - // Lock succeeded. 152 - this.locked = action; 153 - return this; 154 - }).catch(response => { 155 - // Lock failed. 156 - delete this.locked; 157 - // Check if the user can force the lock. 158 - var lockConfirmation = response.responseJSON; 159 - if (response.status === 423 && lockConfirmation) { 160 - // The user can force the lock, but needs confirmation. 161 - this.lockConfirmation = lockConfirmation; 162 - } 163 - return Promise.reject(this); 117 + }).then(function() { 118 + thisXWikiDocument.locked = action; 119 + return thisXWikiDocument; 164 164 }); 165 165 }, 166 166 ... ... @@ -183,25 +183,6 @@ 183 183 // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests 184 184 $.ajax({type: 'GET', url: url, async: false}); 185 185 } 186 - }, 187 - 188 - /** 189 - * Makes sure this document matches the current UI locale. 190 - */ 191 - translate: function() { 192 - const realLocale = this.getRealLocale(); 193 - const uiLocale = $('html').attr('lang'); 194 - if (realLocale && realLocale !== uiLocale) { 195 - this.language = uiLocale; 196 - // Set the original document locale. 197 - this.translations = this.translations || {}; 198 - this.translations['default'] = realLocale; 199 - // Update the document fields that are not 'shared' with the original document. 200 - this.isNew = true; 201 - delete this.version; 202 - delete this.majorVersion; 203 - delete this.minorVersion; 204 - } 205 205 } 206 206 }; 207 207 }); ... ... @@ -218,12 +218,14 @@ 218 218 'xwiki-events-bridge' 219 219 ], function($, xcontext, xwikiDocumentAPI) { 220 220 var preload = function() { 221 - paths.css.forEach(loadCSS); 158 + loadCSS($jsontool.serialize($xwiki.getSkinFile('js/xwiki/actionbuttons/actionButtons.css', true))); 159 + // Required in case the user needs to resolve merge conflicts on save. 160 + loadCSS($jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.css', true))); 222 222 return initActionButtons(); 223 223 }; 224 224 225 225 var loadCSS = function(url) { 226 - $('<link />').attr({165 + var link = $('<link>').attr({ 227 227 type: 'text/css', 228 228 rel: 'stylesheet', 229 229 href: url ... ... @@ -238,12 +238,6 @@ 238 238 }); 239 239 }; 240 240 241 - var translatePage = function() { 242 - return editInPlace({ 243 - afterEdit: createTranslation 244 - }); 245 - }; 246 - 247 247 var editSection = function(sectionId) { 248 248 return editInPlace({ 249 249 lockFailed: function() { ... ... @@ -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() {191 + $('#xwikicontent > #' + escapeSelector(sectionId)).each(function() { 259 259 selectText(this); 260 260 }); 261 261 } ... ... @@ -263,6 +263,22 @@ 263 263 }); 264 264 }; 265 265 199 + var escapeSelector = function(selector) { 200 + if (window.CSS && typeof CSS.escape === 'function') { 201 + // Not supported by Internet Explorer. 202 + return CSS.escape(selector); 203 + } else if (typeof $.escapeSelector === 'function') { 204 + // Added in jQuery 3.0 205 + return $.escapeSelector(selector); 206 + } else if (typeof selector === 'string') { 207 + // Simple implementation. 208 + // 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/ 209 + return selector.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1'); 210 + } else { 211 + return selector; 212 + } 213 + }; 214 + 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() {}, ... ... @@ -281,58 +281,35 @@ 281 281 lockFailed: function() {} 282 282 }, options); 283 283 $('#xwikicontent').addClass('loading'); 284 - // Lock the document first. 285 - return lock(currentXWikiDocument) 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); 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'); 297 - // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly. 228 + return lock(currentXWikiDocument).fail(options.lockFailed) 229 + .then(load) 230 + .then(edit).done(options.afterEdit).always(function() { 231 + $('#xwikicontent').removeClass('loading'); 298 298 }).then(maybeSave) 299 - // Then unlock the document both when the edit ended with success and with a failure. 300 - .then(unlock, unlock) 301 - // Finally view the document both when the edit ended with success and with a failure. 302 - .then(view, view) 303 - // Update the current document for the next edit session. 304 - .then(setCurrentXWikiDocument, setCurrentXWikiDocument); 233 + .then(unlock) 234 + .then(view); 305 305 }; 306 306 307 307 var lock = function(xwikiDocument) { 308 - return xwikiDocument.lock().catch(function(xwikiDocument) { 238 + return xwikiDocument.lock().then(null, function(response) { 239 + var confirmation = response.responseJSON; 309 309 // If the document was already locked then we need to ask the user if they want to force the lock. 310 - if (xwikiDocument.lockConfirmation) { 311 - var confirmation = xwikiDocument.lockConfirmation; 312 - delete xwikiDocument.lockConfirmation; 313 - return maybeForceLock(confirmation).then(xwikiDocument.lock.bind(xwikiDocument, 'edit', true), function() { 314 - // Cancel the edit action. 315 - return Promise.reject(xwikiDocument); 316 - }); 241 + if (response.status === 423 && confirmation) { 242 + return maybeForceLock(confirmation).then($.proxy(xwikiDocument, 'lock', 'edit', true)); 317 317 } else { 318 - new XWiki.widgets.Notification(l10n['edit.inplace.page.lockFailed'], 'error'); 319 - return Promise.reject(xwikiDocument); 244 + new XWiki.widgets.Notification( 245 + $jsontool.serialize($services.localization.render('edit.inplace.page.lockFailed')), 246 + 'error' 247 + ); 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 - }); 253 + 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) {256 + 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; 272 + return deferred.promise(); 352 352 }; 353 353 354 354 var createForceLockModal = function() { ... ... @@ -371,17 +371,17 @@ 371 371 '</div>', 372 372 '</div>' 373 373 ].join('')); 374 - modal.find('.close').attr('aria-label', l 10n['edit.inplace.close']);375 - modal.find('.modal-footer .btn-warning'). on('click',function() {295 + modal.find('.close').attr('aria-label', $jsontool.serialize($services.localization.render('edit.inplace.close'))); 296 + 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,302 + // 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) {305 + if (deferred.state() === 'pending') { 385 385 deferred.reject(); 386 386 } 387 387 }); ... ... @@ -389,19 +389,18 @@ 389 389 }; 390 390 391 391 var load = function(xwikiDocument) { 392 - return xwikiDocument.reload(). then(xwikiDocument=>{313 + 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 => { 400 - new XWiki.widgets.Notification(l10n['edit.inplace.page.loadFailed'], 'error'); 401 - return Promise.reject(xwikiDocument); 319 + }).fail(function() { 320 + new XWiki.widgets.Notification($jsontool.serialize($services.localization.render('edit.inplace.page.loadFailed')), 321 + 'error'); 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));324 + }).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 =>{337 + return waitForAction(xwikiDocument).then(function(action) { 418 418 switch(action.name) { 419 419 case 'save': return save({ 420 420 document: action.document, ... ... @@ -427,76 +427,58 @@ 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 - }); 350 + var deferred = $.Deferred(); 351 + // We wait for the first save, reload or cancel event, whichever is triggered first. Note that the event listeners 352 + // that are not executed first will remain registered but that doesn't cause any problems because the state of a 353 + // deferred object (promise) cannot change once it was resolved. So the first event that fires will resolve the 354 + // promise and the remaining events won't be able to change that. The remaining event listeners could be called 355 + // later but they won't have any effect on the deferred object. 356 + $(document).one([ 357 + 'xwiki:actions:save', 358 + 'xwiki:actions:reload', 359 + 'xwiki:actions:cancel', 360 + ].join(' '), function(event, data) { 361 + deferred.resolve({ 362 + name: event.type.substring('xwiki:actions:'.length), 363 + document: xwikiDocument, 364 + data: data 446 446 }); 447 447 }); 367 + return deferred.promise(); 448 448 }; 449 449 450 450 var save = function(data) { 451 - // Push the changes to the server. 452 - return push(data.document).then(xwikiDocument => { 371 + // Push the changes to the server then render the document for view. We need the view HTML both if we stop editing 372 + // now and if we continue but cancel the edit later. 373 + return push(data.document).then($.proxy(render, null, true)).then(function(xwikiDocument) { 453 453 // Save succeeded. 454 - return shouldReload(xwikiDocument).then( 455 - // The document was saved with merge and thus if we want to continue eding we need to reload the editor (because 456 - // its content doesn't match the saved content). 457 - reload, 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']) 460 - ); 461 - // Save failed. Continue editing because we may have unsaved content. 462 - }, maybeSave); 463 - }; 464 - 465 - var push = function(xwikiDocument) { 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 - }); 476 - }; 477 - 478 - var maybeContinueEditing = function(continueEditing, xwikiDocument) { 479 - var afterReloadAndRender = function(success, xwikiDocument) { 480 - if (continueEditing) { 481 - if (success) { 482 - // Update the original version in order to be able to restore it on cancel. 483 - delete xwikiDocument.originalDocument; 484 - xwikiDocument.originalDocument = $.extend(true, {}, xwikiDocument); 485 - } 375 + if (data['continue']) { 376 + // Update the original version in order to be able to restore it on cancel. 377 + delete xwikiDocument.originalDocument; 378 + xwikiDocument.originalDocument = $.extend(true, {}, xwikiDocument); 486 486 // Continue editing. 487 487 return maybeSave(xwikiDocument); 488 488 } else { 489 - // This is the final version. We stop editing even if the reload / render failed.382 + // This is the final version. 490 490 return xwikiDocument; 491 491 } 492 - }; 385 + }, function(xwikiDocument) { 386 + // Save failed. Continue editing. 387 + return maybeSave(xwikiDocument); 388 + }); 389 + }; 493 493 494 - // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML 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) 499 - ); 391 + var push = function(xwikiDocument) { 392 + // Let actionButtons.js do the push. We just catch the result. 393 + var deferred = $.Deferred(); 394 + // We wait for the save request to either succeed or fail. Note that one of the event listeners will remain 395 + // registered but that doesn't cause any problems because the state of a deferred object (promise) cannot change 396 + // once it was resolved or rejected. So the first event that fires will resolve/reject the promise and the remaining 397 + // event won't be able to change that. The remaining event listener could be called later but it won't have any 398 + // effect. 399 + $(document).one('xwiki:document:saved', $.proxy(deferred, 'resolve', xwikiDocument)); 400 + $(document).one('xwiki:document:saveFailed', $.proxy(deferred, 'reject', xwikiDocument)); 401 + return deferred.promise().then($.proxy(xwikiDocument, 'reload')); 500 500 }; 501 501 502 502 var cancel = function(xwikiDocument) { ... ... @@ -514,27 +514,8 @@ 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));419 + $(window).on('unload pagehide', $.proxy(unlock, null, currentXWikiDocument)); 518 518 519 - var shouldReload = function(xwikiDocument) { 520 - var reloadEventFired = false; 521 - $(document).one('xwiki:actions:reload.maybe', '.xcontent.form', function() { 522 - reloadEventFired = true; 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 - }); 536 - }; 537 - 538 538 var reload = function(xwikiDocument) { 539 539 // Leave the edit mode and then re-enter. 540 540 return view(xwikiDocument, true).then(editInPlace); ... ... @@ -541,33 +541,21 @@ 541 541 }; 542 542 543 543 var view = function(xwikiDocument, reload) { 544 - var viewContent = $('#xwikicontent'); 545 545 // Destroy the editors before returning to view. 546 - viewContent.trigger('xwiki:actions:view', {document: xwikiDocument});428 + $(document).trigger('xwiki:actions:view', {document: xwikiDocument}); 547 547 $('#document-title h1').html(xwikiDocument.renderedTitle); 548 - viewContent.html(xwikiDocument.renderedContent);430 + $('#xwikicontent').html(xwikiDocument.renderedContent); 549 549 if (!reload) { 550 550 // If the user has canceled the edit then the restored page content may include the section edit links. Show them 551 551 // in case they were hidden. 552 - viewContent.children(':header').children('.edit_section').removeClass('hidden');434 + $('#xwikicontent').children(':header').children('.edit_section').removeClass('hidden'); 553 553 // Let others know that the DOM has been updated, in order to enhance it. 554 - $(document).trigger('xwiki:dom:updated', {'elements': viewContent.toArray()});436 + $(document).trigger('xwiki:dom:updated', {'elements': $('#xwikicontent').toArray()}); 555 555 } 556 - // Remove the action events scope. 557 - viewContent.closest('.form').removeClass('form'); 558 - // Update the URL. 559 - if (window.location.hash === '#edit' || window.location.hash === '#translate') { 560 - history.replaceState(null, null, '#'); 561 - } 562 - return Promise.resolve(xwikiDocument); 438 + return $.Deferred().resolve(xwikiDocument).promise(); 563 563 }; 564 564 565 565 var edit = function(xwikiDocument) { 566 - // By adding the 'form' CSS class we set the scope of the action events (e.g. xwiki:actions:beforeSave or 567 - // xwiki:actions:cancel). We need this because in view mode we can have multiple forms active on the page (e.g. one 568 - // for editing the document content in place and one for editing the document syntax in-place) and we don't want 569 - // them to interfere (e.g. canceling one form shouldn't cancel the other forms). 570 - $('#xwikicontent').closest('.xcontent').addClass('form'); 571 571 return initActionButtons(xwikiDocument).then(initTitleEditor).then(initContentEditor) 572 572 .then(startRealTimeEditingSession); 573 573 }; ... ... @@ -574,7 +574,7 @@ 574 574 575 575 var initActionButtons = function(xwikiDocument) { 576 576 if (xwikiDocument) { 577 - initTranslateButton(xwikiDocument);448 + maybeShowTranslateButton(xwikiDocument); 578 578 } 579 579 var editContent = $('#xwikicontent'); 580 580 // We need the wrapper because #xwikicontent uses Bootstrap grid (col-xs-12) which is implemented with CSS float. ... ... @@ -581,185 +581,168 @@ 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); 455 + var actionButtonsWrapper = editContent.nextAll('.sticky-buttons-wrapper'); 456 + if (actionButtonsWrapper.length === 0) { 457 + actionButtonsWrapper = $('<div class="sticky-buttons-wrapper col-xs-12">' + 458 + '<div class="inplace-editing-buttons sticky-buttons"/></div>').insertAfter(editContent); 459 + var actionButtons = actionButtonsWrapper.children('.sticky-buttons').data('xwikiDocument', xwikiDocument) 460 + .toggle(!!xwikiDocument); 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') 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(); 466 + actionButtonsWrapper.children('.sticky-buttons').data('xwikiDocument', xwikiDocument).show(); 467 + // The action buttons are disabled on Save & View. We don't reload the page on Save & View and we reuse the 468 + // action buttons so we need to re-enable them each time we enter the edit mode. 469 + fakeForm.enable(); 470 + $(document).trigger('xwiki:dom:refresh'); 619 619 } 620 - return Promise.resolve(xwikiDocument);472 + return $.Deferred().resolve(xwikiDocument).promise(); 621 621 } 622 622 }; 623 623 624 - var createTranslation = function(xwikiDocument) { 625 - xwikiDocument.translate(); 626 - $('#document-title-input').focus().select(); 627 - // Let the user know that they are now editing the translation of this page in the current locale. 628 - $('#document-title-input').popover({ 629 - content: l10n['edit.inplace.page.translate.messageAfter'], 630 - placement: 'bottom', 631 - trigger: 'manual' 632 - }).popover('show').one('blur', function() { 633 - // Hide the popover when the title input loses the focus. 634 - $(this).popover('hide'); 635 - }); 476 + var maybeShowTranslateButton = function(xwikiDocument) { 477 + var xwikiDocumentLocale = xwikiDocument.getRealLocale(); 478 + var uiLocale = $('html').attr('lang'); 479 + if (xwikiDocumentLocale && xwikiDocumentLocale !== uiLocale) { 480 + $('#tmTranslate').off('click.translate').on('click.translate', function(event) { 481 + event.preventDefault(); 482 + $(this).addClass('hidden'); 483 + xwikiDocument.language = uiLocale; 484 + // Update the document translation fields that are not 'shared' with the original document. 485 + xwikiDocument.isNew = true; 486 + delete xwikiDocument.version; 487 + delete xwikiDocument.majorVersion; 488 + delete xwikiDocument.minorVersion; 489 + $('#document-title-input').focus().select(); 490 + var message = $jsontool.serialize($services.localization.render('edit.inplace.page.translation', 491 + ['__locale__'])); 492 + new XWiki.widgets.Notification( 493 + message.replace('__locale__', uiLocale), 494 + 'info' 495 + ); 496 + }).removeClass('hidden'); 497 + var message = $jsontool.serialize($services.localization.render('edit.inplace.page.original', ['__locale__'])); 498 + new XWiki.widgets.Notification( 499 + message.replace('__locale__', xwikiDocumentLocale), 500 + 'info' 501 + ); 502 + } 636 636 }; 637 637 638 - var initTranslateButton = function(xwikiDocument) { 639 - // Initialize the translate button only if it's visible. 640 - const translateButton = $(config.translateButtonSelector).filter('[data-toggle="popover"]').filter(':visible'); 641 - translateButton.off('click.translate').on('click.translate', function(event) { 642 - event.preventDefault(); 643 - translateButton.parent().addClass('hidden'); 644 - createTranslation(xwikiDocument); 645 - // Let the user know that they are editing the original version of the page and not the translation corresponding 646 - // to the current locale because there isn't one created yet. 647 - }).attr({ 648 - // Backup the initial popover message to be able to restore it on view. 649 - 'data-content-view': translateButton.attr('data-content'), 650 - // Use a custom popover message dedicated to the edit action. 651 - 'data-content': l10n['edit.inplace.page.translate.messageBefore'] 652 - }).popover('show') 653 - // Hide the popover on the next click. The user can still see the message by hovering the translate button. 654 - .closest('html').one('click', function() { 655 - translateButton.popover('hide'); 656 - }); 657 - }; 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); 506 + $(document).on('xwiki:actions:view', function() { 507 + // Hide the action buttons and disable the shortcut keys (by disabling the buttons). 508 + actionButtons.hide().find(':input').prop('disabled', true); 509 + // Hide the translate button because it can be used only in edit mode for the moment. 510 + $('#tmTranslate').addClass('hidden'); 666 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 - $(document).on('xwiki:actions:view', '.xcontent.form', function(event, data) { 672 - // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit 673 - // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing 674 - // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action 675 - // buttons to have them ready for the next editing session (the user can save or cancel and then edit again 676 - // without reloading the page). 677 - actionButtons.find(':input').blur().end().prop('disabled', true).parent().hide(); 678 - // Restore the Translate button if the locale of the viewed document doesn't match the current user interface 679 - // locale (because the viewed document doesn't have a translation in the current locale). 680 - var xwikiDocumentLocale = data.document.getRealLocale(); 681 - var uiLocale = $('html').attr('lang'); 682 - if (xwikiDocumentLocale && xwikiDocumentLocale !== uiLocale) { 683 - const translateButton = $(config.translateButtonSelector).filter('[data-toggle="popover"]'); 684 - // Restore the translation button behavior for view action. 685 - translateButton.off('click.translate') 686 - // Restore the popover text for view action. 687 - .attr('data-content', translateButton.attr('data-content-view') || translateButton.attr('data-content')) 688 - // Restore the visibility. 689 - .parent().removeClass('hidden'); 690 - } 691 - }); 692 - return Promise.resolve($.get(XWiki.currentDocument.getURL('get'), { 512 + return $.get(XWiki.currentDocument.getURL('get'), { 693 693 xpage: 'editactions' 694 - }) ).then(html=>{514 + }).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'); 698 - // Let the others know that the DOM has been updated, in order to enhance it. 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 - }); 518 + // Append the hidden input field that keeps the CSRF token. 519 + $('<input type="hidden" name="form_token" />').val(xcontext.form_token).appendTo(actionButtons); 520 + // We need a place where actionButtons.js can add more hidden inputs. 521 + actionButtons.append('<div class="hidden extra"/>'); 522 + var deferred = $.Deferred(); 523 + require(['actionButtons'], function() { 524 + overrideEditActions(); 525 + overrideAjaxSaveAndContinue(); 526 + var xwikiDocument = actionButtons.data('xwikiDocument'); 527 + // Enable the action buttons (and their shortcut keys) only if we're editing a document. 528 + actionButtons.find(':input').prop('disabled', !xwikiDocument); 529 + deferred.resolve(xwikiDocument); 708 708 }); 709 - }).catch(() => { 710 - new XWiki.widgets.Notification(l10n['edit.inplace.actionButtons.loadFailed'], 'error'); 531 + return deferred.promise(); 532 + }, function() { 533 + new XWiki.widgets.Notification( 534 + $jsontool.serialize($services.localization.render('edit.inplace.actionButtons.loadFailed')), 535 + 'error' 536 + ); 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()); 540 + // actionButtons.js expects a form so we use a fake form. Refactoring actionButtons.js is too dangerous ATM. 541 + var fakeForm = { 542 + action: XWiki.currentDocument.getURL('save'), 543 + async: true, 544 + _getActionButtons: function() { 545 + if (!this._actionButtons) { 546 + this._actionButtons = $('#xwikicontent').nextAll('.sticky-buttons-wrapper').children('.sticky-buttons'); 547 + } 548 + return this._actionButtons; 549 + }, 550 + disable: function() { 551 + this._getActionButtons().find(':input').prop('disabled', true); 552 + }, 553 + enable: function() { 554 + // Clear the extra hidden input fields, that actionButtons.js might have added, each time the form is (re)enabled 555 + // (i.e. after a failed Save & View or before entering the edit mode) because they are designed to be used once. 556 + this._getActionButtons().find('.hidden.extra').empty(); 557 + this._getActionButtons().find(':input').prop('disabled', false); 558 + }, 559 + insert: function(element) { 560 + this._getActionButtons().find('.hidden.extra').append(element); 561 + }, 562 + serialize: function() { 563 + var extra = this._getActionButtons().find(':input').serializeArray().reduce(function(extra, entry) { 564 + var value = extra[entry.name] || []; 565 + value.push(entry.value); 566 + extra[entry.name] = value; 567 + return extra; 568 + }, {}); 569 + var xwikiDocument = this._getActionButtons().data('xwikiDocument'); 570 + var formData = { 571 + title: xwikiDocument.title, 572 + content: xwikiDocument.renderedContent, 573 + RequiresHTMLConversion: 'content', 574 + content_syntax: xwikiDocument.syntax, 575 + language: xwikiDocument.getRealLocale(), 576 + isNew: xwikiDocument.isNew 577 + }; 578 + // Check for merge conflicts only if the document is not new and we know the current version. 579 + if (!xwikiDocument.isNew && xwikiDocument.version) { 580 + formData.previousVersion = xwikiDocument.version; 581 + // It would have been easier to send the timestamp but that's what the Save action expects. 582 + formData.editingVersionDate = new Date(xwikiDocument.modified).toISOString(); 583 + } 584 + return $.extend(formData, extra); 735 735 } 736 736 }; 737 737 588 + var overrideEditActions = function() { 589 + // Override the EditActions.notify() function in order to pass a fake form in the event parameters. 590 + var originalNotify = XWiki.actionButtons.EditActions.prototype.notify; 591 + XWiki.actionButtons.EditActions.prototype.notify = function(originalEvent, action, params) { 592 + if (params && $(originalEvent.element()).closest('.inplace-editing-buttons').length > 0) { 593 + // actionButtons.js expects a form so we use a fake form. Refactoring actionButtons.js is too dangerous ATM. 594 + // Note that we do this only when the event has parameters because we want to exclude the cancel event for which 595 + // actionButtons.js changes the window location if a form is specified, and we want to prevent that. 596 + params.form = fakeForm; 597 + } 598 + return originalNotify.apply(this, arguments); 599 + }; 600 + }; 601 + 738 738 var overrideAjaxSaveAndContinue = function() { 739 739 var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype); 740 740 $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, { 741 741 reloadEditor: function() { 742 - var actionButtons = $('.inplace-editing-buttons'); 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); 606 + if ($('.inplace-editing-buttons').is(':visible')) { 607 + $(document).trigger('xwiki:actions:reload'); 751 751 } else { 752 752 return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments); 753 753 } 754 754 }, 755 - maybeRedirect: function( continueEditing) {612 + maybeRedirect: function() { 756 756 if ($('.inplace-editing-buttons').is(':visible')) { 757 - // Overwrite the default behavior so that we don't redirect when leaving the edit mode because we're already 758 - // in view mode. We still need to report a redirect (return true) if we don't continue editing, so that 759 - // actionButtons.js behaves as if a redirect was done. 760 - return !continueEditing; 614 + // Never redirect when leaving the edit mode because we're already in view mode. 615 + return false; 761 761 } else { 762 - // Fallback on the default behavior if the in-place editing buttons are hidden. 763 763 return originalAjaxSaveAndContinue.maybeRedirect.apply(this, arguments); 764 764 } 765 765 } ... ... @@ -768,29 +768,21 @@ 768 768 769 769 var initTitleEditor = function(xwikiDocument) { 770 770 var label = $('<label for="document-title-input" class="sr-only"/>') 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); 625 + .text($jsontool.serialize($services.localization.render('core.editors.content.titleField.label'))); 626 + var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.title); 627 + var placeholder = xwikiDocument.documentReference.name; 628 + if (placeholder === 'WebHome') { 629 + placeholder = xwikiDocument.documentReference.parent.name; 785 785 } 631 + input.attr('placeholder', placeholder); 786 786 $('#document-title h1').addClass('editable').empty().append([label, input]); 787 - $(document).on('xwiki:actions:beforeSave.titleEditor', '.xcontent.form', function(event) {788 - xwikiDocument. rawTitle = input.val();633 + $(document).on('xwiki:actions:beforeSave.titleEditor', function(event) { 634 + xwikiDocument.title = input.val(); 789 789 }); 790 - $(document).one('xwiki:actions:view', '.xcontent.form', function(event, data) {636 + $(document).one('xwiki:actions:view', function(event, data) { 791 791 // Destroy the title editor. 792 792 $(document).off('xwiki:actions:beforeSave.titleEditor'); 793 - $('#document-title h1').removeClass('editable').text(xwikiDocument. rawTitle);639 + $('#document-title h1').removeClass('editable').text(xwikiDocument.title); 794 794 }); 795 795 return xwikiDocument; 796 796 }; ... ... @@ -807,25 +807,24 @@ 807 807 // Keep the focus while the edit content is being prepared. 808 808 viewContent.focus(); 809 809 } 810 - var data = $.extend({}, config, { 656 + var data = { 657 + contentType: 'org.xwiki.rendering.syntax.SyntaxContent', 658 + editMode: 'wysiwyg', 811 811 document: xwikiDocument, 812 812 // The content editor is loaded on demand, asynchronously. 813 813 deferred: $.Deferred() 814 - } );815 - editContent .trigger('xwiki:actions:edit',data);816 - returndata.deferred.promise().then(()=>{662 + }; 663 + var editContentPromise = data.deferred.promise(); 664 + editContentPromise.done(function() { 817 817 editContent.show(); 818 818 viewContent.remove(); 819 819 if (withFocus) { 820 - // Restore the focus when the edit content is ready but make sure we don't scroll the page. We don't restore the 821 - // focus right away because we just made the content visible so it may not be editable yet (e.g. the WYSIWYG 822 - // editor can make the content editable only if it is visible). 823 - setTimeout(function() { 824 - editContent[0].focus({preventScroll: true}); 825 - }, 0); 668 + // Restore the focus when the edit content is ready but make sure we don't scroll the page. 669 + editContent[0].focus({preventScroll: true}); 826 826 } 827 - return xwikiDocument; 828 828 }); 672 + editContent.trigger('xwiki:actions:edit', data); 673 + return editContentPromise; 829 829 }; 830 830 831 831 var startRealTimeEditingSession = function(xwikiDocument) { ... ... @@ -844,21 +844,16 @@ 844 844 }; 845 845 846 846 return { 847 - preload, 848 - editPage, 849 - editSection, 850 - translatePage 692 + preload: preload, 693 + editPage: editPage, 694 + editSection: editSection 851 851 }; 852 852 }); 853 853 854 854 require(['jquery'], function($) { 855 - // We can edit in-place only if the #xwikicontent element is present. 856 - if (!$('#xwikicontent').length) { 857 - return; 858 - } 699 + var inplaceEditingConfig = $('div[data-inplace-editing-config]').data('inplaceEditingConfig') || {}; 700 + var wysiwygEditorModule = 'xwiki-' + inplaceEditingConfig.wysiwygEditor + '-inline'; 859 859 860 - var wysiwygEditorModule = 'xwiki-' + config.wysiwygEditor + '-inline'; 861 - 862 862 var preloadEditor = function() { 863 863 require(['editInPlace', wysiwygEditorModule], function(editInPlace) { 864 864 editInPlace.preload(); ... ... @@ -875,193 +875,44 @@ 875 875 }); 876 876 } 877 877 878 - var onInPlaceEditing = function(event) { 718 + var editButton = $('#tmEdit > a'); 719 + editButton.on('click.inPlaceEditing', function(event) { 720 + event.preventDefault(); 879 879 // Make sure the user doesn't try to re-activate the edit mode while we are in edit mode. 880 - if (editButton.hasClass('disabled')) { 881 - return; 882 - } 883 - // Disable the edit buttons and hide the section edit links. 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 - $('#xwikicontent').children(':header').children('.edit_section').addClass('hidden'); 722 + editButton.addClass('disabled'); 723 + // Load the code needed to edit in place only when the edit button is clicked. 724 + require(['editInPlace', wysiwygEditorModule], function(editInPlace) { 725 + editInPlace.editPage().always(function() { 726 + editButton.removeClass('disabled'); 727 + }); 728 + // Fallback on the standalone edit mode if we fail to load the required modules. 729 + }, $.proxy(disableInPlaceEditing, event.target)); 730 + }); 731 + 732 + // Section in-place editing. 733 + $('#xwikicontent').on('click.inPlaceEditing', '> :header > a.edit_section:not(.disabled)', function(event) { 890 890 event.preventDefault(); 891 - const handler = event.data; 892 - const data = handler.beforeEdit?.(event); 735 + // Make sure the user doesn't try to re-activate the edit mode while we are in edit mode. 736 + editButton.addClass('disabled'); 737 + // Hide the section editing links and focus the content right away. We could have replaced the section editing icon 738 + // with a loading animation / spinner but giving instant visual feedback about what is going to happen is perceived 739 + // better by the users (it feels faster). 740 + $('#xwikicontent').attr('tabindex', '0').focus().children(':header').children('.edit_section').addClass('hidden'); 741 + var heading = $(event.target).closest(':header'); 893 893 // Load the code needed to edit in place only when the edit button is clicked. 894 894 require(['editInPlace', wysiwygEditorModule], function(editInPlace) { 895 - // Re-enable the translate button because it can be used while editing to create the missing translation. 896 - translateButton.removeClass('disabled'); 897 - handler.edit(editInPlace, data).finally(function() { 898 - // Restore only the edit button at the end because: 899 - // * the translate button is restored (if needed) by the editInPlace module 900 - // * the section edit links are restored when the document is rendered for view 744 + editInPlace.editSection(heading.attr('id')).always(function() { 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));908 - }; 748 + }, $.proxy(disableInPlaceEditing, event.target)); 749 + }); 909 909 910 910 var disableInPlaceEditing = function() { 911 - editButton. add(translateButton).off('click.inPlaceEditing').removeClass('disabled');752 + editButton.off('click.inPlaceEditing').removeClass('disabled'); 912 912 $('#xwikicontent').off('click.inPlaceEditing').removeAttr('tabindex').children(':header').children('.edit_section') 913 913 .removeClass('hidden'); 914 914 // Fallback on the standalone edit mode. 915 915 $(this).click(); 916 916 }; 917 - 918 - var editButton = $(config.editButtonSelector); 919 - editButton.on('click.inPlaceEditing', { 920 - beforeEdit: function() { 921 - history.replaceState(null, null, '#edit'); 922 - }, 923 - edit: function(editInPlace) { 924 - return editInPlace.editPage(); 925 - } 926 - }, onInPlaceEditing).attr('data-editor', 'inplace'); 927 - 928 - var translateButton = $(config.translateButtonSelector); 929 - translateButton.on('click.inPlaceEditing', { 930 - beforeEdit: function() { 931 - history.replaceState(null, null, '#translate'); 932 - translateButton.parent().addClass('hidden'); 933 - }, 934 - edit: function(editInPlace) { 935 - return editInPlace.translatePage(); 936 - } 937 - }, onInPlaceEditing); 938 - 939 - // Section in-place editing. 940 - $('#xwikicontent').on('click.inPlaceEditing', '> :header > a.edit_section:not(.disabled)', { 941 - beforeEdit: function(event) { 942 - // Focus the content right away to give the user instant visual feedback about what is going to happen. 943 - $('#xwikicontent').attr('tabindex', '0').focus(); 944 - // Return the id of the edited section. 945 - return $(event.target).closest(':header').attr('id'); 946 - }, 947 - edit: function(editInPlace, sectionId) { 948 - return editInPlace.editSection(sectionId); 949 - } 950 - }, onInPlaceEditing); 951 - 952 - if (window.location.hash === '#edit') { 953 - editButton.click(); 954 - } else if (window.location.hash === '#translate') { 955 - translateButton.click(); 956 - } 957 957 }); 958 - 959 -require(['jquery'], function($) { 960 - // Backup the document title before each editing session in order to catch changes. 961 - var previousPlainTitle; 962 - $('#xwikicontent').on('xwiki:actions:edit', function(event, data) { 963 - previousPlainTitle = data.document.getPlainTitle(); 964 - }); 965 - 966 - // Update the UI after each editing session. 967 - $(document).on('xwiki:actions:view', function(event, data) { 968 - var xwikiDocument = data.document; 969 - updateDocAuthorAndDate(xwikiDocument); 970 - updateDocExtraTabs(xwikiDocument); 971 - updateDrawer(xwikiDocument); 972 - updateContentMenu(xwikiDocument); 973 - if (xwikiDocument.getPlainTitle() !== previousPlainTitle) { 974 - updateDocTrees(xwikiDocument); 975 - updateLinks(xwikiDocument); 976 - } 977 - }); 978 - 979 - var updateDocAuthorAndDate = function(xwikiDocument) { 980 - var urlWithSelector = xwikiDocument.getURL('get', 'xpage=contentheader') + ' .xdocLastModification'; 981 - $('.xdocLastModification').load(urlWithSelector, function() { 982 - // load() replaces the content of the specified container but we want to replace the container itself. We can't do 983 - // this from the selector, e.g. by using '.xdocLastModification > *' because we lose the text nodes. 984 - $(this).children().unwrap(); 985 - }); 986 - }; 987 - 988 - var updateDocExtraTabs = function(xwikiDocument) { 989 - // Reload the selected tab and force the reload of the hidden tabs next time they are selected. 990 - $('#docextrapanes').children().addClass('empty').empty(); 991 - var selectedTab = $('#docExtraTabs .active[data-template]'); 992 - if (selectedTab.length) { 993 - var docExtraId = selectedTab.attr('id'); 994 - docExtraId = docExtraId.substring(0, docExtraId.length - 'tab'.length); 995 - XWiki.displayDocExtra(docExtraId, selectedTab.data('template'), false); 996 - } 997 - }; 998 - 999 - // Update the document trees (e.g. breadcrumb, navigation) if they have nodes that correspond to the edited document. 1000 - // Note that we want to update the internal tree data not just the link label. This is especially useful if we're 1001 - // going to implement refactoring operations (rename) in the document tree. 1002 - var updateDocTrees = function(xwikiDocument) { 1003 - var plainTitle = xwikiDocument.getPlainTitle(); 1004 - $('.jstree-xwiki').each(function() { 1005 - $(this).jstree?.(true)?.set_text?.('document:' + xwikiDocument.id, plainTitle); 1006 - }); 1007 - }; 1008 - 1009 - // Update the links that target the edited document and whose label matches the document title. Note that this can 1010 - // update links whose label was not generated dynamically (e.g. with server side scripting) based on the document 1011 - // title. For instance there could be links with hard-coded labels or with labels generated using a translatin key 1012 - // (like in the Applications panel). For simplicity we assume that if the link matches the document URL and its 1013 - // previous title then it needs to be updated, but this happens only at the UI level. 1014 - var updateLinks = function(xwikiDocument) { 1015 - var docURL = xwikiDocument.getURL(); 1016 - var newPlainTitle = xwikiDocument.getPlainTitle(); 1017 - // Exclude the links from the document content. 1018 - // Update the links that contain only text (no child elements) otherwise we can lose UI elements (e.g. icons). 1019 - $('a').not('#xwikicontent a').not(':has(*)').filter(function() { 1020 - var linkURL = $(this).attr('href')?.split(/[?#]/, 1)[0]; 1021 - return linkURL === docURL && $(this).text() === previousPlainTitle; 1022 - }).text(newPlainTitle); 1023 - }; 1024 - 1025 - // Update the list of available document translations in the drawer menu. This is needed for instance when a new 1026 - // translation is created using the in-place editor. 1027 - var updateDrawer = function(xwikiDocument) { 1028 - var languageMenu = $('#tmLanguages_menu'); 1029 - var locale = xwikiDocument.getRealLocale(); 1030 - // Look for the language query string parameter, either inside or at the end. 1031 - var localeSelector = 'a[href*="language=' + locale + '&"], a[href$="language=' + locale + '"]'; 1032 - // Check if the language menu is present (multilingual is on) and the document locale is not listed. 1033 - if (languageMenu.length && !languageMenu.find(localeSelector).length) { 1034 - // If we get here then it means a new document translation was created and it needs to be listed in the drawer. 1035 - $('<div/>').load(xwikiDocument.getURL('get', $.param({ 1036 - 'xpage': 'xpart', 1037 - 'vm': 'drawer.vm', 1038 - 'useLayoutVars': true 1039 - // Pass the query string from the current URL so that it gets included in the translation URL. 1040 - // XWIKI-11314: Changing the current language from the UI does not preserve the query string of the current URL 1041 - })) + '&' + location.search.substring(1) + ' #tmLanguages_menu', function() { 1042 - $(this).find('a').each(function() { 1043 - // Clean the query string. 1044 - $(this).attr('href', $(this).attr('href').replace(/&?(xpage=xpart|vm=drawer\.vm|useLayoutVars=true)/g, '') 1045 - .replace('?&', '?')); 1046 - }); 1047 - languageMenu.replaceWith($(this).children()); 1048 - }); 1049 - } 1050 - }; 1051 - 1052 - // Update the links from the content menu to point to the real document locale. This is needed especially when a new 1053 - // document translation is created in-place. 1054 - var updateContentMenu = function(xwikiDocument) { 1055 - var realLocale = xwikiDocument.getRealLocale(); 1056 - var defaultLocale = xwikiDocument.getDefaultLocale(); 1057 - if (realLocale != defaultLocale) { 1058 - var defaultLocaleRegex = new RegExp('(\\blanguage=)' + defaultLocale + '($|&|#)'); 1059 - $('#contentmenu a[href*="language=' + defaultLocale + '"]').each(function() { 1060 - $(this).attr('href', $(this).attr('href').replace(defaultLocaleRegex, '$1' + realLocale + '$2')); 1061 - }); 1062 - } 1063 - }; 1064 -}); 1065 - 1066 -})(JSON.parse(document.querySelector('[data-inplace-editing-config]')?.getAttribute('data-inplace-editing-config')) || 1067 - {}); - Parse content
-
... ... @@ -1,1 +1,1 @@ 1 - No1 +Yes
- XWiki.StyleSheetExtension[0]
-
- Code
-
... ... @@ -6,28 +6,23 @@ 6 6 margin-bottom: @line-height-computed / 4; 7 7 } 8 8 9 -@document-title-input-padding-vertical: @line-height-computed / 4 - 1; 10 10 input#document-title-input { 11 11 /* Preserve the heading styles. */ 11 + border: 1px solid transparent; 12 + box-shadow: none; 12 12 color: inherit; 13 13 font-size: inherit; 14 - background-color: @body-bg; 15 - /* It seems it's not enough to set the line height for the text input. We also need to set its height. */ 16 - height: @font-size-document-title * @headings-line-height + 2 * (1 + @document-title-input-padding-vertical); 15 + height: auto; 17 17 line-height: @headings-line-height; 18 - padding: @ document-title-input-padding-vertical(ceil(@grid-gutter-width / 2) - 1);17 + padding: (@line-height-computed / 4 - 1) (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 {21 +input#document-title-input:hover { 27 27 border-color: @input-border; 28 28 } 29 29 30 -input#document-title-input: valid:focus,25 +input#document-title-input:focus, 31 31 #xwikicontent[contenteditable]:focus, 32 32 #xwikicontent[tabindex]:focus { 33 33 .form-control-focus(); ... ... @@ -52,8 +52,3 @@ 52 52 #xwikicontent { 53 53 padding-top: @line-height-computed * 0.75; 54 54 } 55 - 56 -form#inplace-editing { 57 - /* Leave some space for the bottom box shadow of the editing area. */ 58 - margin-top: 7px; 59 -}
- XWiki.UIExtensionClass[0]
-
- Executed Content
-
... ... @@ -1,69 +9,13 @@ 1 -{{velocity output="false"}} 2 -## TODO: Remove this when XWIKI-18511 (Add support for passing a query string when calling getSkinFile) is implemented. 3 -#macro (getSkinFileWithParams $file $params) 4 -#set ($url = $xwiki.getSkinFile($file, true)) 5 -$url#if ($url.contains('?'))&#else?#end$escapetool.url($params) 6 -#end 7 -{{/velocity}} 8 - 9 9 {{velocity}} 10 10 {{html clean="false"}} 11 -#if ($ services.edit.document.inPlaceEditingEnabled() && $hasEdit && $xcontext.action == 'view' && !$doc.isNew())3 +#if ($xcontext.action == 'view' && !$doc.isNew()) 12 12 ## We support in-place editing only for the WYSIWYG edit mode ATM. 13 13 #getDefaultDocumentEditor($defaultEditMode) 14 14 #if ($defaultEditMode == 'wysiwyg') 15 - #set ($l10nKeys = [ 16 - 'edit.inplace.page.renderFailed', 17 - 'edit.inplace.page.lockFailed', 18 - 'edit.inplace.close', 19 - 'edit.inplace.page.loadFailed', 20 - 'edit.inplace.actionButtons.loadFailed', 21 - 'core.editors.content.titleField.label', 22 - 'core.validation.required.message', 23 - ['edit.inplace.page.translate.messageBefore', $doc.realLocale.getDisplayName($xcontext.locale), 24 - $xcontext.locale.getDisplayName($xcontext.locale)], 25 - ['edit.inplace.page.translate.messageAfter', $xcontext.locale.getDisplayName($xcontext.locale)] 26 - ]) 27 - #set ($l10n = {}) 28 - #foreach ($key in $l10nKeys) 29 - #set ($params = $key.subList(1, $key.size())) 30 - #if ($params) 31 - #set ($discard = $l10n.put($key[0], $services.localization.render($key[0], $params))) 32 - #else 33 - #set ($discard = $l10n.put($key, $services.localization.render($key))) 34 - #end 35 - #end 36 - ## See stylesheets.vm 37 - #set ($cssParams = { 38 - 'skin': $xwiki.skin, 39 - 'colorTheme': $services.model.serialize($themeDoc.documentReference, 'default') 40 - }) 41 - #set ($jsParams = {'language': $xcontext.locale}) 42 - ## We have to explicitly enable the source mode for in-line edit because the latest version of the content editor 43 - ## could be installed on an older version of XWiki where the in-place editor didn't support the source mode (so the 44 - ## content editor cannot enable the source mode by default). 45 45 #set ($inplaceEditingConfig = { 46 - 'contentType': 'org.xwiki.rendering.syntax.SyntaxContent', 47 47 'editMode': $defaultEditMode, 48 - 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id, 49 - 'editButtonSelector': '#tmEdit > a', 50 - 'translateButtonSelector': '#tmTranslate > a', 51 - 'enableSourceMode': true, 52 - 'paths': { 53 - 'js': { 54 - 'xwiki-actionButtons': "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.js' $jsParams)", 55 - 'xwiki-autoSave': "#getSkinFileWithParams('js/xwiki/editors/autosave.js' $jsParams)", 56 - 'xwiki-diff': $xwiki.getSkinFile('uicomponents/viewers/diff.js') 57 - }, 58 - 'css': [ 59 - "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.css' $cssParams)", 60 - "#getSkinFileWithParams('js/xwiki/editors/autosave.css' $cssParams)", 61 - "#getSkinFileWithParams('uicomponents/viewers/diff.css' $cssParams)" 62 - ] 63 - }, 64 - 'l10n': $l10n 9 + 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id 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'))
- XWiki.UIExtensionClass[1]
-
- Cached
-
... ... @@ -1,0 +1,1 @@ 1 +No - Asynchronous rendering
-
... ... @@ -1,0 +1,1 @@ 1 +No - Executed Content
-
... ... @@ -1,0 +1,22 @@ 1 +{{velocity}} 2 +{{html clean="false"}} 3 +## Output the translation button if all the following conditions are met: 4 +## * multilingual is on 5 +## * we're loading the original document version 6 +## * the original document version has a locale specified (it doesn't make sense to translate technical documents) 7 +## * the current UI locale doesn't match the original document locale 8 +#if ($xwiki.isMultiLingual() && $tdoc.realLocale == $doc.realLocale && "$!doc.realLocale" != '' 9 + && $doc.realLocale != $xcontext.locale) 10 + #set ($url = $doc.getURL('edit', $escapetool.url({'language': $xcontext.locale}))) 11 + #set ($hint = $services.localization.render('edit.inplace.page.translate.hint', 12 + [$xcontext.locale.getDisplayName($xcontext.locale)])) 13 + ## We show the translate button only while editing in-place. 14 + <div class="btn-group hidden" id="tmTranslate"> 15 + <a class="btn btn-default" href="$url" role="button" title="$escapetool.xml($hint)"> 16 + $services.icon.renderHTML('translate') 17 + <span class="btn-label">$escapetool.xml($services.localization.render('edit.inplace.page.translate'))</span> 18 + </a> 19 + </div> 20 +#end 21 +{{/html}} 22 +{{/velocity}} - Extension Point ID
-
... ... @@ -1,0 +1,1 @@ 1 +org.xwiki.plaftorm.menu.content - Extension ID
-
... ... @@ -1,0 +1,1 @@ 1 +org.xwiki.plaftorm.menu.content.translate - Extension Parameters
-
... ... @@ -1,0 +1,1 @@ 1 +order=5000 - Extension Scope
-
... ... @@ -1,0 +1,1 @@ 1 +wiki