Changes for page InplaceEditing
From version 1.1
edited by N Pompei
on 08/06/2020 17:32
on 08/06/2020 17:32
Change comment:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/12.4]
To version 4.1
edited by Nazzareno Pompei
on 28/10/2022 08:43
on 28/10/2022 08:43
Change comment:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/14.9]
Summary
-
Page properties (2 modified, 0 added, 0 removed)
-
Objects (3 modified, 0 added, 1 removed)
Details
- Page properties
-
- Author
-
... ... @@ -1,1 +1,1 @@ 1 -XWiki.NPompei 1 +XWiki.NazzarenoPompei - Content
-
... ... @@ -9,11 +9,15 @@ 9 9 #jsonResponse($editConfirmation) 10 10 #else 11 11 ## Lock the document for editing. 12 - #set ($ discard = $response.sendRedirect($tdoc.getURL('lock', $escapetool.url({12 + #set ($lockParams = { 13 13 'ajax': 1, 14 14 'action': $request.lockAction, 15 15 'language': $tdoc.realLocale 16 - })))) 16 + }) 17 + #if ($request.force == 'true') 18 + #set ($lockParams.force = 1) 19 + #end 20 + #set ($discard = $response.sendRedirect($tdoc.getURL('lock', $escapetool.url($lockParams)))) 17 17 #end 18 18 #end 19 19 {{/velocity}}
- XWiki.JavaScriptExtension[0]
-
- Code
-
... ... @@ -1,9 +1,11 @@ 1 +(function(config) { 2 + "use strict"; 3 + 4 +const paths = config.paths; 5 +const l10n = config.l10n; 6 + 1 1 require.config({ 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 - } 8 + paths: paths.js 7 7 }); 8 8 9 9 define('xwiki-document-api', ['jquery'], function($) { ... ... @@ -14,13 +14,36 @@ 14 14 15 15 return { 16 16 /** 19 + * @return this document's plain title 20 + */ 21 + getPlainTitle() { 22 + return $('<div/>').html(this.renderedTitle || this.title).text(); 23 + }, 24 + 25 + /** 17 17 * @return this document's real locale 18 18 */ 19 19 getRealLocale: function() { 20 - return this.language || (this.translations && this.translations['default']) || $('html').attr('lang'); 29 + var realLocale = this.language; 30 + if (typeof realLocale !== 'string' || realLocale === '') { 31 + realLocale = this.getDefaultLocale(); 32 + } 33 + return realLocale; 21 21 }, 22 22 23 23 /** 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 + /** 24 24 * @return the URL that can be used to perform the specified action on this document 25 25 */ 26 26 getURL: function(action, queryString, fragment) { ... ... @@ -55,25 +55,31 @@ 55 55 */ 56 56 render: function(forView) { 57 57 var queryString = { 58 - xpage: ' rendercontent',83 + xpage: 'get', 59 59 outputTitle: true, 60 - outputSyntax: forView ? null : 'annotatedxhtml', 61 61 language: this.getRealLocale(), 62 62 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 63 63 timestamp: new Date().getTime() 64 64 }; 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) { 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. 72 72 var container = $('<div/>').html(html); 73 - return $.extend(this XWikiDocument, {101 + return $.extend(this, { 74 74 renderedTitle: container.find('#document-title h1').html(), 75 75 renderedContent: container.find('#xwikicontent').html() 76 76 }); 105 + }).catch(() => { 106 + new XWiki.widgets.Notification(l10n['edit.inplace.page.renderFailed'], 'error'); 107 + return Promise.reject(this); 77 77 }); 78 78 }, 79 79 ... ... @@ -83,16 +83,19 @@ 83 83 * @return a promise that resolves to this document instance if the reload request succeeds 84 84 */ 85 85 reload: function() { 86 - var thisXWikiDocument = this; 87 - return $.getJSON(this.getRestURL(), { 117 + return Promise.resolve($.getJSON(this.getRestURL(), { 88 88 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 89 89 timestamp: new Date().getTime() 90 - }).then(function(newXWikiDocument) { 120 + })).then(newXWikiDocument => { 121 + // Reload succeeded. 91 91 // Resolve the document reference. 92 - this XWikiDocument.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);123 + this.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT); 93 93 // We were able to load the document so it's not new. 94 - thisXWikiDocument.isNew = false; 95 - return $.extend(thisXWikiDocument, newXWikiDocument); 125 + this.isNew = false; 126 + return $.extend(this, newXWikiDocument); 127 + }).catch(() => { 128 + // Reload failed. 129 + return Promise.reject(this); 96 96 }); 97 97 }, 98 98 ... ... @@ -103,9 +103,8 @@ 103 103 * @return a promise that resolves to this document instance if the lock request succeeds 104 104 */ 105 105 lock: function(action, force) { 106 - var thisXWikiDocument = this; 107 107 action = action || 'edit'; 108 - return $.getJSON(this.getURL('get'), { 141 + return Promise.resolve($.getJSON(this.getURL('get'), { 109 109 sheet: 'XWiki.InplaceEditing', 110 110 action: 'lock', 111 111 lockAction: action, ... ... @@ -114,9 +114,20 @@ 114 114 outputSyntax: 'plain', 115 115 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 116 116 timestamp: new Date().getTime() 117 - }).then(function() { 118 - thisXWikiDocument.locked = action; 119 - return thisXWikiDocument; 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); 120 120 }); 121 121 }, 122 122 ... ... @@ -139,6 +139,25 @@ 139 139 // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests 140 140 $.ajax({type: 'GET', url: url, async: false}); 141 141 } 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 + } 142 142 } 143 143 }; 144 144 }); ... ... @@ -155,9 +155,7 @@ 155 155 'xwiki-events-bridge' 156 156 ], function($, xcontext, xwikiDocumentAPI) { 157 157 var preload = function() { 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))); 221 + paths.css.forEach(loadCSS); 161 161 return initActionButtons(); 162 162 }; 163 163 ... ... @@ -177,6 +177,12 @@ 177 177 }); 178 178 }; 179 179 241 + var translatePage = function() { 242 + return editInPlace({ 243 + afterEdit: createTranslation 244 + }); 245 + }; 246 + 180 180 var editSection = function(sectionId) { 181 181 return editInPlace({ 182 182 lockFailed: function() { ... ... @@ -188,7 +188,7 @@ 188 188 $('#xwikicontent').removeAttr('tabindex'); 189 189 if (sectionId) { 190 190 // Select the heading of the specified section. 191 - $('#xwikicontent > #' + escapeSelector(sectionId)).each(function() { 258 + $('#xwikicontent > #' + $.escapeSelector(sectionId)).each(function() { 192 192 selectText(this); 193 193 }); 194 194 } ... ... @@ -196,22 +196,6 @@ 196 196 }); 197 197 }; 198 198 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 - 215 215 // We preserve the document data between edits in order to be able to know which document translation should be edited 216 216 // (e.g. when the document translation is missing and we create it, the next edit session should target the created 217 217 // translation). ... ... @@ -219,6 +219,11 @@ 219 219 language: xcontext.locale 220 220 }, xwikiDocumentAPI); 221 221 273 + var setCurrentXWikiDocument = function(xwikiDocument) { 274 + currentXWikiDocument = xwikiDocument; 275 + return Promise.resolve(xwikiDocument); 276 + }; 277 + 222 222 var editInPlace = function(options) { 223 223 options = $.extend({ 224 224 afterEdit: function() {}, ... ... @@ -225,35 +225,57 @@ 225 225 lockFailed: function() {} 226 226 }, options); 227 227 $('#xwikicontent').addClass('loading'); 228 - return lock(currentXWikiDocument).fail(options.lockFailed) 229 - .then(load) 230 - .then(edit).done(options.afterEdit).always(function() { 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(() => { 231 231 $('#xwikicontent').removeClass('loading'); 296 + // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly. 232 232 }).then(maybeSave) 233 - .then(unlock) 234 - .then(view); 298 + // Then unlock the document both when the edit ended with success and with a failure. 299 + .then(unlock, unlock) 300 + // Finally view the document both when the edit ended with success and with a failure. 301 + .then(view, view) 302 + // Update the current document for the next edit session. 303 + .then(setCurrentXWikiDocument, setCurrentXWikiDocument); 235 235 }; 236 236 237 237 var lock = function(xwikiDocument) { 238 - return xwikiDocument.lock().then(null, function(response) { 239 - var confirmation = response.responseJSON; 307 + return xwikiDocument.lock().catch(function(xwikiDocument) { 240 240 // If the document was already locked then we need to ask the user if they want to force the lock. 241 - if (response.status === 423 && confirmation) { 242 - return maybeForceLock(confirmation).then($.proxy(xwikiDocument, 'lock', 'edit', true)); 309 + if (xwikiDocument.lockConfirmation) { 310 + var confirmation = xwikiDocument.lockConfirmation; 311 + delete xwikiDocument.lockConfirmation; 312 + return maybeForceLock(confirmation).then(xwikiDocument.lock.bind(xwikiDocument, 'edit', true), function() { 313 + // Cancel the edit action. 314 + return Promise.reject(xwikiDocument); 315 + }); 243 243 } else { 244 - new XWiki.widgets.Notification( 245 - $jsontool.serialize($services.localization.render('edit.inplace.page.lockFailed')), 246 - 'error' 247 - ); 317 + new XWiki.widgets.Notification(l10n['edit.inplace.page.lockFailed'], 'error'); 318 + return Promise.reject(xwikiDocument); 248 248 } 249 249 }); 250 250 }; 251 251 252 252 var maybeForceLock = function(confirmation) { 253 - var deferred = $.Deferred(); 324 + var deferred, promise = new Promise((resolve, reject) => { 325 + deferred = {resolve, reject}; 326 + }); 327 + // We need the catch() to prevent the "Uncaught (in promise)" error log in the console. 328 + promise.catch(() => {}).finally(() => { 329 + // This flag is used by the Force Lock modal to know whether the promise is settled when the modal is closing. 330 + deferred.settled = true; 331 + }); 254 254 // Reuse the confirmation modal once it is created. 255 255 var modal = $('.force-edit-lock-modal'); 256 - if (modal.length === 0) {334 + if (!modal.length) { 257 257 modal = createForceLockModal(); 258 258 } 259 259 // Update the deferred that needs to be resolved or rejected. ... ... @@ -269,7 +269,7 @@ 269 269 } 270 270 // Show the confirmation modal. 271 271 modal.modal('show'); 272 - return deferred.promise();350 + return promise; 273 273 }; 274 274 275 275 var createForceLockModal = function() { ... ... @@ -292,17 +292,17 @@ 292 292 '</div>', 293 293 '</div>' 294 294 ].join('')); 295 - modal.find('.close').attr('aria-label', $jsontool.serialize($services.localization.render('edit.inplace.close')));296 - modal.find('.modal-footer .btn-warning').click (function() {373 + modal.find('.close').attr('aria-label', l10n['edit.inplace.close']); 374 + modal.find('.modal-footer .btn-warning').on('click', function() { 297 297 // The user has confirmed they want to force the lock. 298 298 modal.data('deferred').resolve(); 299 299 modal.modal('hide'); 300 300 }); 301 301 modal.on('hide.bs.modal', function() { 302 - // If the lock promise is not yet resolved when the modal is closing then it means the modal was canceled,380 + // If the lock promise is not yet settled when the modal is closing then it means the modal was canceled, 303 303 // i.e. the user doesn't want to force the lock. 304 304 var deferred = modal.data('deferred'); 305 - if (deferred.st ate() === 'pending') {383 + if (!deferred.settled) { 306 306 deferred.reject(); 307 307 } 308 308 }); ... ... @@ -310,18 +310,19 @@ 310 310 }; 311 311 312 312 var load = function(xwikiDocument) { 313 - return xwikiDocument.reload(). done(function(xwikiDocument){391 + return xwikiDocument.reload().then(xwikiDocument => { 314 314 // Clone the current document version and keep a reference to it in order to be able to restore it on cancel. 315 315 xwikiDocument.originalDocument = $.extend(true, { 316 316 renderedTitle: $('#document-title h1').html(), 317 317 renderedContent: $('#xwikicontent').html() 318 318 }, xwikiDocument); 319 - }).fail(function() { 320 - new XWiki.widgets.Notification($jsontool.serialize($services.localization.render('edit.inplace.page.loadFailed')), 321 - 'error'); 397 + return xwikiDocument; 398 + }).catch(xwikiDocument => { 399 + new XWiki.widgets.Notification(l10n['edit.inplace.page.loadFailed'], 'error'); 400 + return Promise.reject(xwikiDocument); 322 322 // Render the document for edit, in order to have the annotated content HTML. The annotations are used to protect 323 323 // the rendering transformations (e.g. macros) when editing the content. 324 - }).then( $.proxy(render,null, false));403 + }).then(render.bind(null, false)); 325 325 }; 326 326 327 327 /** ... ... @@ -334,7 +334,7 @@ 334 334 }; 335 335 336 336 var maybeSave = function(xwikiDocument) { 337 - return waitForAction(xwikiDocument).then( function(action){416 + return waitForAction(xwikiDocument).then(action => { 338 338 switch(action.name) { 339 339 case 'save': return save({ 340 340 document: action.document, ... ... @@ -347,58 +347,76 @@ 347 347 }; 348 348 349 349 var waitForAction = function(xwikiDocument) { 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 429 + return new Promise((resolve, reject) => { 430 + // We wait for the first save, reload or cancel event, whichever is triggered first. Note that the event listeners 431 + // that are not executed first will remain registered but that doesn't cause any problems because the state of a 432 + // deferred object (promise) cannot change once it was resolved. So the first event that fires will resolve the 433 + // promise and the remaining events won't be able to change that. The remaining event listeners could be called 434 + // later but they won't have any effect on the deferred object. 435 + $(document).one([ 436 + 'xwiki:actions:save', 437 + 'xwiki:actions:reload', 438 + 'xwiki:actions:cancel', 439 + ].join(' '), '.xcontent.form', function(event, data) { 440 + resolve({ 441 + name: event.type.substring('xwiki:actions:'.length), 442 + document: xwikiDocument, 443 + data: data 444 + }); 365 365 }); 366 366 }); 367 - return deferred.promise(); 368 368 }; 369 369 370 370 var save = function(data) { 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) { 450 + // Push the changes to the server. 451 + return push(data.document).then(xwikiDocument => { 374 374 // Save succeeded. 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); 453 + return shouldReload(xwikiDocument).then( 454 + // The document was saved with merge and thus if we want to continue eding we need to reload the editor (because 455 + // its content doesn't match the saved content). 456 + reload, 457 + // No need to reload the editor because either the action was Save & View or there was no merge on save. 458 + maybeContinueEditing.bind(null, data['continue']) 459 + ); 460 + // Save failed. Continue editing because we may have unsaved content. 461 + }, maybeSave); 462 + }; 463 + 464 + var push = function(xwikiDocument) { 465 + // Let actionButtons.js do the push. We just catch the result. 466 + return new Promise((resolve, reject) => { 467 + // We wait for the save request to either succeed or fail. Note that one of the event listeners will remain 468 + // registered but that doesn't cause any problems because the state of a deferred object (promise) cannot change 469 + // once it was resolved or rejected. So the first event that fires will resolve/reject the promise and the 470 + // remaining event won't be able to change that. The remaining event listener could be called later but it won't 471 + // have any effect. 472 + $(document).one('xwiki:document:saved', '.xcontent.form', resolve.bind(null, xwikiDocument)); 473 + $(document).one('xwiki:document:saveFailed', '.xcontent.form', reject.bind(null, xwikiDocument)); 474 + }); 475 + }; 476 + 477 + var maybeContinueEditing = function(continueEditing, xwikiDocument) { 478 + var afterReloadAndRender = function(success, xwikiDocument) { 479 + if (continueEditing) { 480 + if (success) { 481 + // Update the original version in order to be able to restore it on cancel. 482 + delete xwikiDocument.originalDocument; 483 + xwikiDocument.originalDocument = $.extend(true, {}, xwikiDocument); 484 + } 379 379 // Continue editing. 380 380 return maybeSave(xwikiDocument); 381 381 } else { 382 - // This is the final version. 488 + // This is the final version. We stop editing even if the reload / render failed. 383 383 return xwikiDocument; 384 384 } 385 - }, function(xwikiDocument) { 386 - // Save failed. Continue editing. 387 - return maybeSave(xwikiDocument); 388 - }); 389 - }; 491 + }; 390 390 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')); 493 + // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML 494 + // both if we stop editing now and if we continue but cancel the edit later. 495 + return xwikiDocument.reload().then(render.bind(null, true)).then( 496 + afterReloadAndRender.bind(null, /* success: */ true), 497 + afterReloadAndRender.bind(null, /* success: */ false) 498 + ); 402 402 }; 403 403 404 404 var cancel = function(xwikiDocument) { ... ... @@ -416,8 +416,27 @@ 416 416 }; 417 417 418 418 // Make sure we unlock the document when the user navigates to another page. 419 - $(window).on('unload pagehide', $.proxy(unlock,null, currentXWikiDocument));516 + $(window).on('unload pagehide', unlock.bind(null, currentXWikiDocument)); 420 420 518 + var shouldReload = function(xwikiDocument) { 519 + var reloadEventFired = false; 520 + $(document).one('xwiki:actions:reload.maybe', '.xcontent.form', function() { 521 + reloadEventFired = true; 522 + }); 523 + return new Promise((resolve, reject) => { 524 + // Wait a bit to see if the reload event is fired. 525 + setTimeout(function() { 526 + // Remove the listener in case the reload event wasn't fired. 527 + $(document).off('xwiki:actions:reload.maybe'); 528 + if (reloadEventFired) { 529 + resolve(xwikiDocument); 530 + } else { 531 + reject(xwikiDocument); 532 + } 533 + }, 0); 534 + }); 535 + }; 536 + 421 421 var reload = function(xwikiDocument) { 422 422 // Leave the edit mode and then re-enter. 423 423 return view(xwikiDocument, true).then(editInPlace); ... ... @@ -424,21 +424,33 @@ 424 424 }; 425 425 426 426 var view = function(xwikiDocument, reload) { 543 + var viewContent = $('#xwikicontent'); 427 427 // Destroy the editors before returning to view. 428 - $(document).trigger('xwiki:actions:view', {document: xwikiDocument});545 + viewContent.trigger('xwiki:actions:view', {document: xwikiDocument}); 429 429 $('#document-title h1').html(xwikiDocument.renderedTitle); 430 - $('#xwikicontent').html(xwikiDocument.renderedContent);547 + viewContent.html(xwikiDocument.renderedContent); 431 431 if (!reload) { 432 432 // If the user has canceled the edit then the restored page content may include the section edit links. Show them 433 433 // in case they were hidden. 434 - $('#xwikicontent').children(':header').children('.edit_section').removeClass('hidden');551 + viewContent.children(':header').children('.edit_section').removeClass('hidden'); 435 435 // Let others know that the DOM has been updated, in order to enhance it. 436 - $(document).trigger('xwiki:dom:updated', {'elements': $('#xwikicontent').toArray()});553 + $(document).trigger('xwiki:dom:updated', {'elements': viewContent.toArray()}); 437 437 } 438 - return $.Deferred().resolve(xwikiDocument).promise(); 555 + // Remove the action events scope. 556 + viewContent.closest('.form').removeClass('form'); 557 + // Update the URL. 558 + if (window.location.hash === '#edit' || window.location.hash === '#translate') { 559 + history.replaceState(null, null, '#'); 560 + } 561 + return Promise.resolve(xwikiDocument); 439 439 }; 440 440 441 441 var edit = function(xwikiDocument) { 565 + // By adding the 'form' CSS class we set the scope of the action events (e.g. xwiki:actions:beforeSave or 566 + // xwiki:actions:cancel). We need this because in view mode we can have multiple forms active on the page (e.g. one 567 + // for editing the document content in place and one for editing the document syntax in-place) and we don't want 568 + // them to interfere (e.g. canceling one form shouldn't cancel the other forms). 569 + $('#xwikicontent').closest('.xcontent').addClass('form'); 442 442 return initActionButtons(xwikiDocument).then(initTitleEditor).then(initContentEditor) 443 443 .then(startRealTimeEditingSession); 444 444 }; ... ... @@ -445,7 +445,7 @@ 445 445 446 446 var initActionButtons = function(xwikiDocument) { 447 447 if (xwikiDocument) { 448 - maybeShowTranslateButton(xwikiDocument);576 + initTranslateButton(xwikiDocument); 449 449 } 450 450 var editContent = $('#xwikicontent'); 451 451 // We need the wrapper because #xwikicontent uses Bootstrap grid (col-xs-12) which is implemented with CSS float. ... ... @@ -455,63 +455,90 @@ 455 455 var actionButtonsWrapper = editContent.nextAll('.sticky-buttons-wrapper'); 456 456 if (actionButtonsWrapper.length === 0) { 457 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); 586 + '<div class="inplace-editing-buttons sticky-buttons"/></div>').insertAfter(editContent).toggle(!!xwikiDocument); 587 + var actionButtons = actionButtonsWrapper.children('.sticky-buttons') 588 + .data('xwikiDocument', xwikiDocument) 589 + // Expose the fake form if an extension needs to manipulate it. 590 + .data('fakeForm', fakeForm); 461 461 return loadActionButtons(actionButtons); 462 462 } else { 463 463 // If we're editing a page.. 464 464 if (xwikiDocument) { 465 465 // ..then make sure the action buttons are displayed right away (don't wait for the user to scroll). 466 - actionButtonsWrapper.children('.sticky-buttons').data('xwikiDocument', xwikiDocument).show(); 596 + actionButtonsWrapper.show().children('.sticky-buttons') 597 + .data('xwikiDocument', xwikiDocument) 598 + // Expose the fake form if an extension needs to manipulate it. 599 + .data('fakeForm', fakeForm) 600 + // but make sure the position of the action buttons is updated. 601 + .trigger('xwiki:dom:refresh'); 467 467 // The action buttons are disabled on Save & View. We don't reload the page on Save & View and we reuse the 468 468 // action buttons so we need to re-enable them each time we enter the edit mode. 469 469 fakeForm.enable(); 470 - $(document).trigger('xwiki:dom:refresh'); 471 471 } 472 - return $.Deferred().resolve(xwikiDocument).promise();606 + return Promise.resolve(xwikiDocument); 473 473 } 474 474 }; 475 475 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 - } 610 + var createTranslation = function(xwikiDocument) { 611 + xwikiDocument.translate(); 612 + $('#document-title-input').focus().select(); 613 + // Let the user know that they are now editing the translation of this page in the current locale. 614 + $('#document-title-input').popover({ 615 + content: l10n['edit.inplace.page.translate.messageAfter'], 616 + placement: 'bottom', 617 + trigger: 'manual' 618 + }).popover('show').one('blur', function() { 619 + // Hide the popover when the title input loses the focus. 620 + $(this).popover('hide'); 621 + }); 503 503 }; 504 504 624 + var initTranslateButton = function(xwikiDocument) { 625 + // Initialize the translate button only if it's visible. 626 + const translateButton = $(config.translateButtonSelector).filter('[data-toggle="popover"]').filter(':visible'); 627 + translateButton.off('click.translate').on('click.translate', function(event) { 628 + event.preventDefault(); 629 + translateButton.parent().addClass('hidden'); 630 + createTranslation(xwikiDocument); 631 + // Let the user know that they are editing the original version of the page and not the translation corresponding 632 + // to the current locale because there isn't one created yet. 633 + }).attr({ 634 + // Backup the initial popover message to be able to restore it on view. 635 + 'data-content-view': translateButton.attr('data-content'), 636 + // Use a custom popover message dedicated to the edit action. 637 + 'data-content': l10n['edit.inplace.page.translate.messageBefore'] 638 + }).popover('show') 639 + // Hide the popover on the next click. The user can still see the message by hovering the translate button. 640 + .closest('html').one('click', function() { 641 + translateButton.popover('hide'); 642 + }); 643 + }; 644 + 505 505 var loadActionButtons = function(actionButtons) { 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'); 646 + $(document).on('xwiki:actions:view', '.xcontent.form', function(event, data) { 647 + // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit 648 + // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing 649 + // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action 650 + // buttons to have them ready for the next editing session (the user can save or cancel and then edit again 651 + // without reloading the page). 652 + actionButtons.find(':input').blur().prop('disabled', true).end().parent().hide(); 653 + // Restore the Translate button if the locale of the viewed document doesn't match the current user interface 654 + // locale (because the viewed document doesn't have a translation in the current locale). 655 + var xwikiDocumentLocale = data.document.getRealLocale(); 656 + var uiLocale = $('html').attr('lang'); 657 + if (xwikiDocumentLocale && xwikiDocumentLocale !== uiLocale) { 658 + const translateButton = $(config.translateButtonSelector).filter('[data-toggle="popover"]'); 659 + // Restore the translation button behavior for view action. 660 + translateButton.off('click.translate') 661 + // Restore the popover text for view action. 662 + .attr('data-content', translateButton.attr('data-content-view') || translateButton.attr('data-content')) 663 + // Restore the visibility. 664 + .parent().removeClass('hidden'); 665 + } 511 511 }); 512 - return $.get(XWiki.currentDocument.getURL('get'), { 667 + return Promise.resolve($.get(XWiki.currentDocument.getURL('get'), { 513 513 xpage: 'editactions' 514 - }).then( function(html){669 + })).then(html => { 515 515 actionButtons.html(html); 516 516 // Fix the name of the Save & View action. 517 517 actionButtons.find('.btn-primary').first().attr('name', 'action_save'); ... ... @@ -519,21 +519,24 @@ 519 519 $('<input type="hidden" name="form_token" />').val(xcontext.form_token).appendTo(actionButtons); 520 520 // We need a place where actionButtons.js can add more hidden inputs. 521 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); 677 + // Let the others know that the DOM has been updated, in order to enhance it. 678 + $(document).trigger('xwiki:dom:updated', {'elements': actionButtons.toArray()}); 679 + return new Promise((resolve, reject) => { 680 + require(['xwiki-actionButtons', 'xwiki-diff', 'xwiki-autoSave'], function() { 681 + overrideEditActions(); 682 + overrideAjaxSaveAndContinue(); 683 + // Activate the auto-save feature passing our fake edit form. Note that autosave.js also creates an instance of 684 + // AutoSave but it doesn't do anything because it doesn't find a real edit form in the page. This is why we have 685 + // to create our own instance of AutoSave passing the right (fake) form. 686 + new XWiki.editors.AutoSave({form: fakeForm}); 687 + var xwikiDocument = actionButtons.data('xwikiDocument'); 688 + // Enable the action buttons (and their shortcut keys) only if we're editing a document. 689 + actionButtons.find(':input').prop('disabled', !xwikiDocument); 690 + resolve(xwikiDocument); 691 + }); 530 530 }); 531 - return deferred.promise(); 532 - }, function() { 533 - new XWiki.widgets.Notification( 534 - $jsontool.serialize($services.localization.render('edit.inplace.actionButtons.loadFailed')), 535 - 'error' 536 - ); 693 + }).catch(() => { 694 + new XWiki.widgets.Notification(l10n['edit.inplace.actionButtons.loadFailed'], 'error'); 537 537 }); 538 538 }; 539 539 ... ... @@ -559,6 +559,13 @@ 559 559 insert: function(element) { 560 560 this._getActionButtons().find('.hidden.extra').append(element); 561 561 }, 720 + // Note that this method only works with single argument. 721 + append: function(element) { 722 + this.insert(element); 723 + }, 724 + down: function(selector) { 725 + return this._getActionButtons().find(selector)[0]; 726 + }, 562 562 serialize: function() { 563 563 var extra = this._getActionButtons().find(':input').serializeArray().reduce(function(extra, entry) { 564 564 var value = extra[entry.name] || []; ... ... @@ -566,22 +566,39 @@ 566 566 extra[entry.name] = value; 567 567 return extra; 568 568 }, {}); 734 + // retrieve all input fields listing the temporary uploaded files. 735 + var uploadedFiles = $('#xwikicontent').nextAll('input[name="uploadedFiles"]').serializeArray().reduce(function(extra, entry) { 736 + var value = extra[entry.name] || []; 737 + value.push(entry.value); 738 + extra[entry.name] = value; 739 + return extra; 740 + }, {}); 569 569 var xwikiDocument = this._getActionButtons().data('xwikiDocument'); 570 570 var formData = { 571 - title: xwikiDocument.title, 572 - content: xwikiDocument.renderedContent, 573 - RequiresHTMLConversion: 'content', 574 - content_syntax: xwikiDocument.syntax, 743 + title: xwikiDocument.rawTitle, 575 575 language: xwikiDocument.getRealLocale(), 576 576 isNew: xwikiDocument.isNew 577 577 }; 747 + if (xwikiDocument.content != xwikiDocument.originalDocument.content) { 748 + // Submit the raw (source) content. No syntax conversion is needed in this case. 749 + formData.content = xwikiDocument.content; 750 + } else { 751 + // Submit the rendered content (HTML), but make sure it is converted to the document syntax on the server. 752 + $.extend(formData, { 753 + content: xwikiDocument.renderedContent, 754 + RequiresHTMLConversion: 'content', 755 + content_syntax: xwikiDocument.syntax 756 + }); 757 + } 758 + // Add the temporary uploaded files to the form. 759 + $.extend(formData, uploadedFiles); 578 578 // Check for merge conflicts only if the document is not new and we know the current version. 579 579 if (!xwikiDocument.isNew && xwikiDocument.version) { 580 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(); 763 + formData.editingVersionDate = new Date(xwikiDocument.modified).getTime(); 583 583 } 584 - return $.extend(formData, extra); 765 + // Ensure that formData information has priority over extra information. 766 + return $.extend({}, extra, formData); 585 585 } 586 586 }; 587 587 ... ... @@ -603,17 +603,21 @@ 603 603 var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype); 604 604 $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, { 605 605 reloadEditor: function() { 606 - if ($('.inplace-editing-buttons').is(':visible')) { 607 - $(document).trigger('xwiki:actions:reload'); 788 + var actionButtons = $('.inplace-editing-buttons'); 789 + if (actionButtons.is(':visible')) { 790 + actionButtons.trigger('xwiki:actions:reload'); 608 608 } else { 609 609 return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments); 610 610 } 611 611 }, 612 - maybeRedirect: function() { 795 + maybeRedirect: function(continueEditing) { 613 613 if ($('.inplace-editing-buttons').is(':visible')) { 614 - // Never redirect when leaving the edit mode because we're already in view mode. 615 - return false; 797 + // Overwrite the default behavior so that we don't redirect when leaving the edit mode because we're already 798 + // in view mode. We still need to report a redirect (return true) if we don't continue editing, so that 799 + // actionButtons.js behaves as if a redirect was done. 800 + return !continueEditing; 616 616 } else { 802 + // Fallback on the default behavior if the in-place editing buttons are hidden. 617 617 return originalAjaxSaveAndContinue.maybeRedirect.apply(this, arguments); 618 618 } 619 619 } ... ... @@ -622,8 +622,8 @@ 622 622 623 623 var initTitleEditor = function(xwikiDocument) { 624 624 var label = $('<label for="document-title-input" class="sr-only"/>') 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);811 + .text(l10n['core.editors.content.titleField.label']); 812 + var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.rawTitle); 627 627 var placeholder = xwikiDocument.documentReference.name; 628 628 if (placeholder === 'WebHome') { 629 629 placeholder = xwikiDocument.documentReference.parent.name; ... ... @@ -630,13 +630,13 @@ 630 630 } 631 631 input.attr('placeholder', placeholder); 632 632 $('#document-title h1').addClass('editable').empty().append([label, input]); 633 - $(document).on('xwiki:actions:beforeSave.titleEditor', function(event) { 634 - xwikiDocument. title = input.val();819 + $(document).on('xwiki:actions:beforeSave.titleEditor', '.xcontent.form', function(event) { 820 + xwikiDocument.rawTitle = input.val(); 635 635 }); 636 - $(document).one('xwiki:actions:view', function(event, data) { 822 + $(document).one('xwiki:actions:view', '.xcontent.form', function(event, data) { 637 637 // Destroy the title editor. 638 638 $(document).off('xwiki:actions:beforeSave.titleEditor'); 639 - $('#document-title h1').removeClass('editable').text(xwikiDocument. title);825 + $('#document-title h1').removeClass('editable').text(xwikiDocument.rawTitle); 640 640 }); 641 641 return xwikiDocument; 642 642 }; ... ... @@ -653,24 +653,25 @@ 653 653 // Keep the focus while the edit content is being prepared. 654 654 viewContent.focus(); 655 655 } 656 - var data = { 657 - contentType: 'org.xwiki.rendering.syntax.SyntaxContent', 658 - editMode: 'wysiwyg', 842 + var data = $.extend({}, config, { 659 659 document: xwikiDocument, 660 660 // The content editor is loaded on demand, asynchronously. 661 661 deferred: $.Deferred() 662 - }; 663 - vareditContentPromise=data.deferred.promise();664 - e ditContentPromise.done(function() {846 + }); 847 + editContent.trigger('xwiki:actions:edit', data); 848 + return data.deferred.promise().then(() => { 665 665 editContent.show(); 666 666 viewContent.remove(); 667 667 if (withFocus) { 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}); 852 + // Restore the focus when the edit content is ready but make sure we don't scroll the page. We don't restore the 853 + // focus right away because we just made the content visible so it may not be editable yet (e.g. the WYSIWYG 854 + // editor can make the content editable only if it is visible). 855 + setTimeout(function() { 856 + editContent[0].focus({preventScroll: true}); 857 + }, 0); 670 670 } 859 + return xwikiDocument; 671 671 }); 672 - editContent.trigger('xwiki:actions:edit', data); 673 - return editContentPromise; 674 674 }; 675 675 676 676 var startRealTimeEditingSession = function(xwikiDocument) { ... ... @@ -689,16 +689,21 @@ 689 689 }; 690 690 691 691 return { 692 - preload: preload, 693 - editPage: editPage, 694 - editSection: editSection 879 + preload, 880 + editPage, 881 + editSection, 882 + translatePage 695 695 }; 696 696 }); 697 697 698 698 require(['jquery'], function($) { 699 - var inplaceEditingConfig = $('div[data-inplace-editing-config]').data('inplaceEditingConfig') || {}; 700 - var wysiwygEditorModule = 'xwiki-' + inplaceEditingConfig.wysiwygEditor + '-inline'; 887 + // We can edit in-place only if the #xwikicontent element is present. 888 + if (!$('#xwikicontent').length) { 889 + return; 890 + } 701 701 892 + var wysiwygEditorModule = 'xwiki-' + config.wysiwygEditor + '-inline'; 893 + 702 702 var preloadEditor = function() { 703 703 require(['editInPlace', wysiwygEditorModule], function(editInPlace) { 704 704 editInPlace.preload(); ... ... @@ -715,44 +715,186 @@ 715 715 }); 716 716 } 717 717 718 - var editButton = $('#tmEdit > a'); 719 - editButton.on('click.inPlaceEditing', function(event) { 720 - event.preventDefault(); 910 + var onInPlaceEditing = function(event) { 721 721 // Make sure the user doesn't try to re-activate the edit mode while we are in edit mode. 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) { 912 + if (editButton.hasClass('disabled')) { 913 + return; 914 + } 915 + // Disable the edit buttons and hide the section edit links. 916 + editButton.add(translateButton).addClass('disabled'); 917 + $('#xwikicontent').children(':header').children('.edit_section').addClass('hidden'); 734 734 event.preventDefault(); 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'); 919 + const handler = event.data; 920 + const data = handler.beforeEdit?.(event); 742 742 // Load the code needed to edit in place only when the edit button is clicked. 743 743 require(['editInPlace', wysiwygEditorModule], function(editInPlace) { 744 - editInPlace.editSection(heading.attr('id')).always(function() { 923 + // Re-enable the translate button because it can be used while editing to create the missing translation. 924 + translateButton.removeClass('disabled'); 925 + handler.edit(editInPlace, data).finally(function() { 926 + // Restore only the edit button at the end because: 927 + // * the translate button is restored (if needed) by the editInPlace module 928 + // * the section edit links are restored when the document is rendered for view 745 745 editButton.removeClass('disabled'); 746 746 }); 747 747 // Fallback on the standalone edit mode if we fail to load the required modules. 748 - }, $.proxy(disableInPlaceEditing,event.target));749 - } );932 + }, disableInPlaceEditing.bind(event.target)); 933 + }; 750 750 751 751 var disableInPlaceEditing = function() { 752 - editButton.off('click.inPlaceEditing').removeClass('disabled'); 936 + editButton.add(translateButton).off('click.inPlaceEditing').removeClass('disabled'); 753 753 $('#xwikicontent').off('click.inPlaceEditing').removeAttr('tabindex').children(':header').children('.edit_section') 754 754 .removeClass('hidden'); 755 755 // Fallback on the standalone edit mode. 756 756 $(this).click(); 757 757 }; 942 + 943 + var editButton = $(config.editButtonSelector); 944 + editButton.on('click.inPlaceEditing', { 945 + beforeEdit: function() { 946 + history.replaceState(null, null, '#edit'); 947 + }, 948 + edit: function(editInPlace) { 949 + return editInPlace.editPage(); 950 + } 951 + }, onInPlaceEditing).attr('data-editor', 'inplace'); 952 + 953 + var translateButton = $(config.translateButtonSelector); 954 + translateButton.on('click.inPlaceEditing', { 955 + beforeEdit: function() { 956 + history.replaceState(null, null, '#translate'); 957 + translateButton.parent().addClass('hidden'); 958 + }, 959 + edit: function(editInPlace) { 960 + return editInPlace.translatePage(); 961 + } 962 + }, onInPlaceEditing); 963 + 964 + // Section in-place editing. 965 + $('#xwikicontent').on('click.inPlaceEditing', '> :header > a.edit_section:not(.disabled)', { 966 + beforeEdit: function(event) { 967 + // Focus the content right away to give the user instant visual feedback about what is going to happen. 968 + $('#xwikicontent').attr('tabindex', '0').focus(); 969 + // Return the id of the edited section. 970 + return $(event.target).closest(':header').attr('id'); 971 + }, 972 + edit: function(editInPlace, sectionId) { 973 + return editInPlace.editSection(sectionId); 974 + } 975 + }, onInPlaceEditing); 976 + 977 + if (window.location.hash === '#edit') { 978 + editButton.click(); 979 + } else if (window.location.hash === '#translate') { 980 + translateButton.click(); 981 + } 758 758 }); 983 + 984 +require(['jquery'], function($) { 985 + // Backup the document title before each editing session in order to catch changes. 986 + var previousPlainTitle; 987 + $('#xwikicontent').on('xwiki:actions:edit', function(event, data) { 988 + previousPlainTitle = data.document.getPlainTitle(); 989 + }); 990 + 991 + // Update the UI after each editing session. 992 + $(document).on('xwiki:actions:view', function(event, data) { 993 + var xwikiDocument = data.document; 994 + updateDocAuthorAndDate(xwikiDocument); 995 + updateDocExtraTabs(xwikiDocument); 996 + updateDrawer(xwikiDocument); 997 + updateContentMenu(xwikiDocument); 998 + if (xwikiDocument.getPlainTitle() !== previousPlainTitle) { 999 + updateDocTrees(xwikiDocument); 1000 + updateLinks(xwikiDocument); 1001 + } 1002 + }); 1003 + 1004 + var updateDocAuthorAndDate = function(xwikiDocument) { 1005 + var urlWithSelector = xwikiDocument.getURL('get', 'xpage=contentheader') + ' .xdocLastModification'; 1006 + $('.xdocLastModification').load(urlWithSelector, function() { 1007 + // load() replaces the content of the specified container but we want to replace the container itself. We can't do 1008 + // this from the selector, e.g. by using '.xdocLastModification > *' because we lose the text nodes. 1009 + $(this).children().unwrap(); 1010 + }); 1011 + }; 1012 + 1013 + var updateDocExtraTabs = function(xwikiDocument) { 1014 + // Reload the selected tab and force the reload of the hidden tabs next time they are selected. 1015 + $('#docextrapanes').children().addClass('empty').empty(); 1016 + var selectedTab = $('#docExtraTabs .active[data-template]'); 1017 + if (selectedTab.length) { 1018 + var docExtraId = selectedTab.attr('id'); 1019 + docExtraId = docExtraId.substring(0, docExtraId.length - 'tab'.length); 1020 + XWiki.displayDocExtra(docExtraId, selectedTab.data('template'), false); 1021 + } 1022 + }; 1023 + 1024 + // Update the document trees (e.g. breadcrumb, navigation) if they have nodes that correspond to the edited document. 1025 + // Note that we want to update the internal tree data not just the link label. This is especially useful if we're 1026 + // going to implement refactoring operations (rename) in the document tree. 1027 + var updateDocTrees = function(xwikiDocument) { 1028 + var plainTitle = xwikiDocument.getPlainTitle(); 1029 + $('.jstree-xwiki').each(function() { 1030 + $(this).jstree?.(true)?.set_text?.('document:' + xwikiDocument.id, plainTitle); 1031 + }); 1032 + }; 1033 + 1034 + // Update the links that target the edited document and whose label matches the document title. Note that this can 1035 + // update links whose label was not generated dynamically (e.g. with server side scripting) based on the document 1036 + // title. For instance there could be links with hard-coded labels or with labels generated using a translatin key 1037 + // (like in the Applications panel). For simplicity we assume that if the link matches the document URL and its 1038 + // previous title then it needs to be updated, but this happens only at the UI level. 1039 + var updateLinks = function(xwikiDocument) { 1040 + var docURL = xwikiDocument.getURL(); 1041 + var newPlainTitle = xwikiDocument.getPlainTitle(); 1042 + // Exclude the links from the document content. 1043 + // Update the links that contain only text (no child elements) otherwise we can lose UI elements (e.g. icons). 1044 + $('a').not('#xwikicontent a').not(':has(*)').filter(function() { 1045 + var linkURL = $(this).attr('href')?.split(/[?#]/, 1)[0]; 1046 + return linkURL === docURL && $(this).text() === previousPlainTitle; 1047 + }).text(newPlainTitle); 1048 + }; 1049 + 1050 + // Update the list of available document translations in the drawer menu. This is needed for instance when a new 1051 + // translation is created using the in-place editor. 1052 + var updateDrawer = function(xwikiDocument) { 1053 + var languageMenu = $('#tmLanguages_menu'); 1054 + var locale = xwikiDocument.getRealLocale(); 1055 + // Look for the language query string parameter, either inside or at the end. 1056 + var localeSelector = 'a[href*="language=' + locale + '&"], a[href$="language=' + locale + '"]'; 1057 + // Check if the language menu is present (multilingual is on) and the document locale is not listed. 1058 + if (languageMenu.length && !languageMenu.find(localeSelector).length) { 1059 + // If we get here then it means a new document translation was created and it needs to be listed in the drawer. 1060 + $('<div/>').load(xwikiDocument.getURL('get', $.param({ 1061 + 'xpage': 'xpart', 1062 + 'vm': 'drawer.vm', 1063 + 'useLayoutVars': true 1064 + // Pass the query string from the current URL so that it gets included in the translation URL. 1065 + // XWIKI-11314: Changing the current language from the UI does not preserve the query string of the current URL 1066 + })) + '&' + location.search.substring(1) + ' #tmLanguages_menu', function() { 1067 + $(this).find('a').each(function() { 1068 + // Clean the query string. 1069 + $(this).attr('href', $(this).attr('href').replace(/&?(xpage=xpart|vm=drawer\.vm|useLayoutVars=true)/g, '') 1070 + .replace('?&', '?')); 1071 + }); 1072 + languageMenu.replaceWith($(this).children()); 1073 + }); 1074 + } 1075 + }; 1076 + 1077 + // Update the links from the content menu to point to the real document locale. This is needed especially when a new 1078 + // document translation is created in-place. 1079 + var updateContentMenu = function(xwikiDocument) { 1080 + var realLocale = xwikiDocument.getRealLocale(); 1081 + var defaultLocale = xwikiDocument.getDefaultLocale(); 1082 + if (realLocale != defaultLocale) { 1083 + var defaultLocaleRegex = new RegExp('(\\blanguage=)' + defaultLocale + '($|&|#)'); 1084 + $('#contentmenu a[href*="language=' + defaultLocale + '"]').each(function() { 1085 + $(this).attr('href', $(this).attr('href').replace(defaultLocaleRegex, '$1' + realLocale + '$2')); 1086 + }); 1087 + } 1088 + }; 1089 +}); 1090 + 1091 +})(JSON.parse(document.querySelector('[data-inplace-editing-config]')?.getAttribute('data-inplace-editing-config')) || 1092 + {}); - Parse content
-
... ... @@ -1,1 +1,1 @@ 1 - Yes1 +No
- XWiki.StyleSheetExtension[0]
-
- Code
-
... ... @@ -6,6 +6,7 @@ 6 6 margin-bottom: @line-height-computed / 4; 7 7 } 8 8 9 +@document-title-input-padding-vertical: @line-height-computed / 4 - 1; 9 9 input#document-title-input { 10 10 /* Preserve the heading styles. */ 11 11 border: 1px solid transparent; ... ... @@ -12,9 +12,10 @@ 12 12 box-shadow: none; 13 13 color: inherit; 14 14 font-size: inherit; 15 - height: auto; 16 + /* It seems it's not enough to set the line height for the text input. We also need to set its height. */ 17 + height: @font-size-document-title * @headings-line-height + 2 * (1 + @document-title-input-padding-vertical); 16 16 line-height: @headings-line-height; 17 - padding: (@line-height-computed/ 4-1)(ceil(@grid-gutter-width / 2) - 1);19 + padding: @document-title-input-padding-vertical (ceil(@grid-gutter-width / 2) - 1); 18 18 width: 100%; 19 19 } 20 20 ... ... @@ -47,3 +47,8 @@ 47 47 #xwikicontent { 48 48 padding-top: @line-height-computed * 0.75; 49 49 } 52 + 53 +.sticky-buttons-wrapper { 54 + /* Leave some space for the bottom box shadow of the editing area. */ 55 + margin-top: 7px; 56 +}
- XWiki.UIExtensionClass[0]
-
- Executed Content
-
... ... @@ -1,12 +1,66 @@ 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 + 1 1 {{velocity}} 2 2 {{html clean="false"}} 3 -#if ($xcontext.action == 'view' && !$doc.isNew()) 11 +#if ($services.edit.document.inPlaceEditingEnabled() && $hasEdit && $xcontext.action == 'view' && !$doc.isNew()) 4 4 ## We support in-place editing only for the WYSIWYG edit mode ATM. 5 5 #getDefaultDocumentEditor($defaultEditMode) 6 6 #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 + ['edit.inplace.page.translate.messageBefore', $doc.realLocale.getDisplayName($xcontext.locale), 23 + $xcontext.locale.getDisplayName($xcontext.locale)], 24 + ['edit.inplace.page.translate.messageAfter', $xcontext.locale.getDisplayName($xcontext.locale)] 25 + ]) 26 + #set ($l10n = {}) 27 + #foreach ($key in $l10nKeys) 28 + #set ($params = $key.subList(1, $key.size())) 29 + #if ($params) 30 + #set ($discard = $l10n.put($key[0], $services.localization.render($key[0], $params))) 31 + #else 32 + #set ($discard = $l10n.put($key, $services.localization.render($key))) 33 + #end 34 + #end 35 + ## See stylesheets.vm 36 + #set ($cssParams = { 37 + 'skin': $xwiki.skin, 38 + 'colorTheme': $services.model.serialize($themeDoc.documentReference, 'default') 39 + }) 40 + #set ($jsParams = {'language': $xcontext.locale}) 41 + ## We have to explicitly enable the source mode for in-line edit because the latest version of the content editor 42 + ## could be installed on an older version of XWiki where the in-place editor didn't support the source mode (so the 43 + ## content editor cannot enable the source mode by default). 7 7 #set ($inplaceEditingConfig = { 45 + 'contentType': 'org.xwiki.rendering.syntax.SyntaxContent', 8 8 'editMode': $defaultEditMode, 9 - 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id 47 + 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id, 48 + 'editButtonSelector': '#tmEdit > a', 49 + 'translateButtonSelector': '#tmTranslate > a', 50 + 'enableSourceMode': true, 51 + 'paths': { 52 + 'js': { 53 + 'xwiki-actionButtons': "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.js' $jsParams)", 54 + 'xwiki-autoSave': "#getSkinFileWithParams('js/xwiki/editors/autosave.js' $jsParams)", 55 + 'xwiki-diff': $xwiki.getSkinFile('uicomponents/viewers/diff.js') 56 + }, 57 + 'css': [ 58 + "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.css' $cssParams)", 59 + "#getSkinFileWithParams('js/xwiki/editors/autosave.css' $cssParams)", 60 + "#getSkinFileWithParams('uicomponents/viewers/diff.css' $cssParams)" 61 + ] 62 + }, 63 + 'l10n': $l10n 10 10 }) 11 11 <div class="hidden" data-inplace-editing-config="$escapetool.xml($jsontool.serialize($inplaceEditingConfig))"></div> 12 12 ## We didn't move this to the file system because it uses LESS and we didn't want to include it in the skin.
- XWiki.UIExtensionClass[1]
-
- Cached
-
... ... @@ -1,1 +1,0 @@ 1 -No - Asynchronous rendering
-
... ... @@ -1,1 +1,0 @@ 1 -No - Executed Content
-
... ... @@ -1,22 +1,0 @@ 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,1 +1,0 @@ 1 -org.xwiki.plaftorm.menu.content - Extension ID
-
... ... @@ -1,1 +1,0 @@ 1 -org.xwiki.plaftorm.menu.content.translate - Extension Parameters
-
... ... @@ -1,1 +1,0 @@ 1 -order=5000 - Extension Scope
-
... ... @@ -1,1 +1,0 @@ 1 -wiki