Modifiche per il documento InplaceEditing
Dalla versione 2.1
modificato da N Pompei
il 16/09/2020 12:29
il 16/09/2020 12:29
Cambia il commento:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/12.7.1]
Alla versione 6.1
modificato da Nazzareno Pompei
il 19/01/2024 08:57
il 19/01/2024 08:57
Cambia il commento:
Install extension [org.xwiki.platform:xwiki-platform-edit-ui/15.10.5]
Summary
-
Page properties (2 modified, 0 added, 0 removed)
-
Objects (3 modified, 0 added, 1 removed)
Details
- Page properties
-
- Autore del documento
-
... ... @@ -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]
-
- Codice
-
... ... @@ -1,10 +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 - 'autoSave': $jsontool.serialize($xwiki.getSkinFile('js/xwiki/editors/autosave.js', true)), 5 - // Required in case the user needs to resolve merge conflicts on save. 6 - 'xwiki-diff': $jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.js')) 7 - } 8 + paths: paths.js 8 8 }); 9 9 10 10 define('xwiki-document-api', ['jquery'], function($) { ... ... @@ -15,23 +15,36 @@ 15 15 16 16 return { 17 17 /** 19 + * @return this document's plain title 20 + */ 21 + getPlainTitle() { 22 + return $('<div/>').html(this.renderedTitle || this.title).text(); 23 + }, 24 + 25 + /** 18 18 * @return this document's real locale 19 19 */ 20 20 getRealLocale: function() { 21 21 var realLocale = this.language; 22 - if (typeof realLocale === 'string' && realLocale !== '') { 23 - // This document is a translation. 24 - } else if (this.translations && typeof this.translations['default'] === 'string') { 25 - // This is the original document. 26 - realLocale = this.translations['default']; 27 - } else { 28 - // The document locale is not specified. Use the UI locale. 29 - realLocale = $('html').attr('lang'); 30 + if (typeof realLocale !== 'string' || realLocale === '') { 31 + realLocale = this.getDefaultLocale(); 30 30 } 31 31 return realLocale; 32 32 }, 33 33 34 34 /** 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 + /** 35 35 * @return the URL that can be used to perform the specified action on this document 36 36 */ 37 37 getURL: function(action, queryString, fragment) { ... ... @@ -68,27 +68,29 @@ 68 68 var queryString = { 69 69 xpage: 'get', 70 70 outputTitle: true, 71 - outputSyntax: forView ? null : 'annotatedxhtml', 72 72 language: this.getRealLocale(), 73 73 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 74 74 timestamp: new Date().getTime() 75 75 }; 76 - var thisXWikiDocument = this; 77 - return $.get(this.getURL('view'), queryString).fail(function() { 78 - new XWiki.widgets.Notification( 79 - $jsontool.serialize($services.localization.render('edit.inplace.page.renderFailed')), 80 - 'error' 81 - ); 82 - }).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 => { 83 83 // Render succeeded. 84 84 var container = $('<div/>').html(html); 85 - return $.extend(this XWikiDocument, {101 + return $.extend(this, { 86 86 renderedTitle: container.find('#document-title h1').html(), 87 87 renderedContent: container.find('#xwikicontent').html() 88 88 }); 89 - } , function() {90 - //Renderfailed.91 - return thisXWikiDocument;105 + }).catch(() => { 106 + new XWiki.widgets.Notification(l10n['edit.inplace.page.renderFailed'], 'error'); 107 + return Promise.reject(this); 92 92 }); 93 93 }, 94 94 ... ... @@ -98,20 +98,19 @@ 98 98 * @return a promise that resolves to this document instance if the reload request succeeds 99 99 */ 100 100 reload: function() { 101 - var thisXWikiDocument = this; 102 - return $.getJSON(this.getRestURL(), { 117 + return Promise.resolve($.getJSON(this.getRestURL(), { 103 103 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 104 104 timestamp: new Date().getTime() 105 - }).then( function(newXWikiDocument){120 + })).then(newXWikiDocument => { 106 106 // Reload succeeded. 107 107 // Resolve the document reference. 108 - this XWikiDocument.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);123 + this.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT); 109 109 // We were able to load the document so it's not new. 110 - this XWikiDocument.isNew = false;111 - return $.extend(this XWikiDocument, newXWikiDocument);112 - } , function() {125 + this.isNew = false; 126 + return $.extend(this, newXWikiDocument); 127 + }).catch(() => { 113 113 // Reload failed. 114 - return thisXWikiDocument;129 + return Promise.reject(this); 115 115 }); 116 116 }, 117 117 ... ... @@ -122,9 +122,8 @@ 122 122 * @return a promise that resolves to this document instance if the lock request succeeds 123 123 */ 124 124 lock: function(action, force) { 125 - var thisXWikiDocument = this; 126 126 action = action || 'edit'; 127 - return $.getJSON(this.getURL('get'), { 141 + return Promise.resolve($.getJSON(this.getURL('get'), { 128 128 sheet: 'XWiki.InplaceEditing', 129 129 action: 'lock', 130 130 lockAction: action, ... ... @@ -133,20 +133,20 @@ 133 133 outputSyntax: 'plain', 134 134 // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). 135 135 timestamp: new Date().getTime() 136 - }).then( function() {150 + })).then(() => { 137 137 // Lock succeeded. 138 - this XWikiDocument.locked = action;139 - return this XWikiDocument;140 - } , function(response){152 + this.locked = action; 153 + return this; 154 + }).catch(response => { 141 141 // Lock failed. 142 - delete this XWikiDocument.locked;156 + delete this.locked; 143 143 // Check if the user can force the lock. 144 144 var lockConfirmation = response.responseJSON; 145 145 if (response.status === 423 && lockConfirmation) { 146 146 // The user can force the lock, but needs confirmation. 147 - this XWikiDocument.lockConfirmation = lockConfirmation;161 + this.lockConfirmation = lockConfirmation; 148 148 } 149 - return thisXWikiDocument;163 + return Promise.reject(this); 150 150 }); 151 151 }, 152 152 ... ... @@ -169,6 +169,25 @@ 169 169 // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests 170 170 $.ajax({type: 'GET', url: url, async: false}); 171 171 } 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 + } 172 172 } 173 173 }; 174 174 }); ... ... @@ -185,15 +185,12 @@ 185 185 'xwiki-events-bridge' 186 186 ], function($, xcontext, xwikiDocumentAPI) { 187 187 var preload = function() { 188 - loadCSS($jsontool.serialize($xwiki.getSkinFile('js/xwiki/actionbuttons/actionButtons.css', true))); 189 - loadCSS($jsontool.serialize($xwiki.getSkinFile('js/xwiki/editors/autosave.css', true))); 190 - // Required in case the user needs to resolve merge conflicts on save. 191 - loadCSS($jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.css', true))); 221 + paths.css.forEach(loadCSS); 192 192 return initActionButtons(); 193 193 }; 194 194 195 195 var loadCSS = function(url) { 196 - var link =$('<link>').attr({226 + $('<link/>').attr({ 197 197 type: 'text/css', 198 198 rel: 'stylesheet', 199 199 href: url ... ... @@ -208,6 +208,12 @@ 208 208 }); 209 209 }; 210 210 241 + var translatePage = function() { 242 + return editInPlace({ 243 + afterEdit: createTranslation 244 + }); 245 + }; 246 + 211 211 var editSection = function(sectionId) { 212 212 return editInPlace({ 213 213 lockFailed: function() { ... ... @@ -219,7 +219,7 @@ 219 219 $('#xwikicontent').removeAttr('tabindex'); 220 220 if (sectionId) { 221 221 // Select the heading of the specified section. 222 - $('#xwikicontent > #' + escapeSelector(sectionId)).each(function() { 258 + $('#xwikicontent > #' + $.escapeSelector(sectionId)).each(function() { 223 223 selectText(this); 224 224 }); 225 225 } ... ... @@ -227,22 +227,6 @@ 227 227 }); 228 228 }; 229 229 230 - var escapeSelector = function(selector) { 231 - if (window.CSS && typeof CSS.escape === 'function') { 232 - // Not supported by Internet Explorer. 233 - return CSS.escape(selector); 234 - } else if (typeof $.escapeSelector === 'function') { 235 - // Added in jQuery 3.0 236 - return $.escapeSelector(selector); 237 - } else if (typeof selector === 'string') { 238 - // Simple implementation. 239 - // 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/ 240 - return selector.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1'); 241 - } else { 242 - return selector; 243 - } 244 - }; 245 - 246 246 // We preserve the document data between edits in order to be able to know which document translation should be edited 247 247 // (e.g. when the document translation is missing and we create it, the next edit session should target the created 248 248 // translation). ... ... @@ -250,6 +250,11 @@ 250 250 language: xcontext.locale 251 251 }, xwikiDocumentAPI); 252 252 273 + var setCurrentXWikiDocument = function(xwikiDocument) { 274 + currentXWikiDocument = xwikiDocument; 275 + return Promise.resolve(xwikiDocument); 276 + }; 277 + 253 253 var editInPlace = function(options) { 254 254 options = $.extend({ 255 255 afterEdit: function() {}, ... ... @@ -257,42 +257,57 @@ 257 257 }, options); 258 258 $('#xwikicontent').addClass('loading'); 259 259 // Lock the document first. 260 - return lock(currentXWikiDocument) .fail(options.lockFailed)285 + return lock(currentXWikiDocument) 261 261 // Then load the document only if we managed to lock it. 262 - .then(load) 287 + .then(load, xwikiDocument => { 288 + options.lockFailed(xwikiDocument); 289 + return Promise.reject(xwikiDocument); 263 263 // Then load the editors only if we managed to load the document. 264 - .then(edit).done(options.afterEdit).always(function() { 265 - $('#xwikicontent').removeClass('loading'); 291 + }).then(edit).then(xwikiDocument => { 292 + options.afterEdit(xwikiDocument); 293 + return xwikiDocument; 294 + }).finally(() => { 295 + // Remove the aria-expanded attribute which is incorrect for role=textbox 296 + $('#xwikicontent').removeClass('loading').removeAttr('aria-expanded'); 266 266 // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly. 267 267 }).then(maybeSave) 268 268 // Then unlock the document both when the edit ended with success and with a failure. 269 269 .then(unlock, unlock) 270 270 // Finally view the document both when the edit ended with success and with a failure. 271 - .then(view, view); 302 + .then(view, view) 303 + // Update the current document for the next edit session. 304 + .then(setCurrentXWikiDocument, setCurrentXWikiDocument); 272 272 }; 273 273 274 274 var lock = function(xwikiDocument) { 275 - return xwikiDocument.lock().th en(null,function(xwikiDocument) {308 + return xwikiDocument.lock().catch(function(xwikiDocument) { 276 276 // If the document was already locked then we need to ask the user if they want to force the lock. 277 277 if (xwikiDocument.lockConfirmation) { 278 278 var confirmation = xwikiDocument.lockConfirmation; 279 279 delete xwikiDocument.lockConfirmation; 280 - return maybeForceLock(confirmation).then($.proxy(xwikiDocument, 'lock', 'edit', true)); 313 + return maybeForceLock(confirmation).then(xwikiDocument.lock.bind(xwikiDocument, 'edit', true), function() { 314 + // Cancel the edit action. 315 + return Promise.reject(xwikiDocument); 316 + }); 281 281 } else { 282 - new XWiki.widgets.Notification( 283 - $jsontool.serialize($services.localization.render('edit.inplace.page.lockFailed')), 284 - 'error' 285 - ); 286 - return xwikiDocument; 318 + new XWiki.widgets.Notification(l10n['edit.inplace.page.lockFailed'], 'error'); 319 + return Promise.reject(xwikiDocument); 287 287 } 288 288 }); 289 289 }; 290 290 291 291 var maybeForceLock = function(confirmation) { 292 - var deferred = $.Deferred(); 325 + var deferred, promise = new Promise((resolve, reject) => { 326 + deferred = {resolve, reject}; 327 + }); 328 + // We need the catch() to prevent the "Uncaught (in promise)" error log in the console. 329 + promise.catch(() => {}).finally(() => { 330 + // This flag is used by the Force Lock modal to know whether the promise is settled when the modal is closing. 331 + deferred.settled = true; 332 + }); 293 293 // Reuse the confirmation modal once it is created. 294 294 var modal = $('.force-edit-lock-modal'); 295 - if (modal.length === 0) {335 + if (!modal.length) { 296 296 modal = createForceLockModal(); 297 297 } 298 298 // Update the deferred that needs to be resolved or rejected. ... ... @@ -308,7 +308,7 @@ 308 308 } 309 309 // Show the confirmation modal. 310 310 modal.modal('show'); 311 - return deferred.promise();351 + return promise; 312 312 }; 313 313 314 314 var createForceLockModal = function() { ... ... @@ -331,17 +331,17 @@ 331 331 '</div>', 332 332 '</div>' 333 333 ].join('')); 334 - modal.find('.close').attr('aria-label', $jsontool.serialize($services.localization.render('edit.inplace.close')));335 - modal.find('.modal-footer .btn-warning').click (function() {374 + modal.find('.close').attr('aria-label', l10n['edit.inplace.close']); 375 + modal.find('.modal-footer .btn-warning').on('click', function() { 336 336 // The user has confirmed they want to force the lock. 337 337 modal.data('deferred').resolve(); 338 338 modal.modal('hide'); 339 339 }); 340 340 modal.on('hide.bs.modal', function() { 341 - // If the lock promise is not yet resolved when the modal is closing then it means the modal was canceled,381 + // If the lock promise is not yet settled when the modal is closing then it means the modal was canceled, 342 342 // i.e. the user doesn't want to force the lock. 343 343 var deferred = modal.data('deferred'); 344 - if (deferred.st ate() === 'pending') {384 + if (!deferred.settled) { 345 345 deferred.reject(); 346 346 } 347 347 }); ... ... @@ -349,18 +349,19 @@ 349 349 }; 350 350 351 351 var load = function(xwikiDocument) { 352 - return xwikiDocument.reload(). done(function(xwikiDocument){392 + return xwikiDocument.reload().then(xwikiDocument => { 353 353 // Clone the current document version and keep a reference to it in order to be able to restore it on cancel. 354 354 xwikiDocument.originalDocument = $.extend(true, { 355 355 renderedTitle: $('#document-title h1').html(), 356 356 renderedContent: $('#xwikicontent').html() 357 357 }, xwikiDocument); 358 - }).fail(function() { 359 - new XWiki.widgets.Notification($jsontool.serialize($services.localization.render('edit.inplace.page.loadFailed')), 360 - 'error'); 398 + return xwikiDocument; 399 + }).catch(xwikiDocument => { 400 + new XWiki.widgets.Notification(l10n['edit.inplace.page.loadFailed'], 'error'); 401 + return Promise.reject(xwikiDocument); 361 361 // Render the document for edit, in order to have the annotated content HTML. The annotations are used to protect 362 362 // the rendering transformations (e.g. macros) when editing the content. 363 - }).then( $.proxy(render,null, false));404 + }).then(render.bind(null, false)); 364 364 }; 365 365 366 366 /** ... ... @@ -373,7 +373,7 @@ 373 373 }; 374 374 375 375 var maybeSave = function(xwikiDocument) { 376 - return waitForAction(xwikiDocument).then( function(action){417 + return waitForAction(xwikiDocument).then(action => { 377 377 switch(action.name) { 378 378 case 'save': return save({ 379 379 document: action.document, ... ... @@ -386,29 +386,29 @@ 386 386 }; 387 387 388 388 var waitForAction = function(xwikiDocument) { 389 - var deferred = $.Deferred(); 390 - // We wait for the first save, reload or cancel event, whichever is triggered first. Note that the event listeners 391 - // that are not executed first will remain registered but that doesn't cause any problems because the state of a 392 - // deferred object (promise) cannot change once it was resolved. So the first event that fires will resolve the 393 - // promise and the remaining events won't be able to change that. The remaining event listeners could be called 394 - // later but they won't have any effect on the deferred object. 395 - $(document).one([ 396 - 'xwiki:actions:save', 397 - 'xwiki:actions:reload', 398 - 'xwiki:actions:cancel', 399 - ].join(' '), function(event, data) { 400 - deferred.resolve({ 401 - name: event.type.substring('xwiki:actions:'.length), 402 - document: xwikiDocument, 403 - data: data 430 + return new Promise((resolve, reject) => { 431 + // We wait for the first save, reload or cancel event, whichever is triggered first. Note that the event listeners 432 + // that are not executed first will remain registered but that doesn't cause any problems because the state of a 433 + // deferred object (promise) cannot change once it was resolved. So the first event that fires will resolve the 434 + // promise and the remaining events won't be able to change that. The remaining event listeners could be called 435 + // later but they won't have any effect on the deferred object. 436 + $(document).one([ 437 + 'xwiki:actions:save', 438 + 'xwiki:actions:reload', 439 + 'xwiki:actions:cancel', 440 + ].join(' '), '.xcontent.form', function(event, data) { 441 + resolve({ 442 + name: event.type.substring('xwiki:actions:'.length), 443 + document: xwikiDocument, 444 + data: data 445 + }); 404 404 }); 405 405 }); 406 - return deferred.promise(); 407 407 }; 408 408 409 409 var save = function(data) { 410 410 // Push the changes to the server. 411 - return push(data.document).then( function(xwikiDocument){452 + return push(data.document).then(xwikiDocument => { 412 412 // Save succeeded. 413 413 return shouldReload(xwikiDocument).then( 414 414 // The document was saved with merge and thus if we want to continue eding we need to reload the editor (because ... ... @@ -415,7 +415,7 @@ 415 415 // its content doesn't match the saved content). 416 416 reload, 417 417 // No need to reload the editor because either the action was Save & View or there was no merge on save. 418 - $.proxy(maybeContinueEditing,null, data['continue'])459 + maybeContinueEditing.bind(null, data['continue']) 419 419 ); 420 420 // Save failed. Continue editing because we may have unsaved content. 421 421 }, maybeSave); ... ... @@ -423,15 +423,15 @@ 423 423 424 424 var push = function(xwikiDocument) { 425 425 // Let actionButtons.js do the push. We just catch the result. 426 - vardeferred= $.Deferred();427 - // We wait for the save request to either succeed or fail. Note that one of the event listeners will remain 428 - // registered but that doesn't cause any problems because the state of a deferred object (promise) cannot change 429 - // once it was resolved or rejected. So the first event that fires will resolve/reject the promise and the remaining430 - // event won't be able to change that. The remaining event listener could be called later but it won't have any431 - // effect. 432 - $(document).one('xwiki:document:saved', $.proxy(deferred,'resolve', xwikiDocument));433 - $(document).one('xwiki:document:saveFailed', $.proxy(deferred,'reject', xwikiDocument));434 - return deferred.promise();467 + return new Promise((resolve, reject) => { 468 + // We wait for the save request to either succeed or fail. Note that one of the event listeners will remain 469 + // registered but that doesn't cause any problems because the state of a deferred object (promise) cannot change 470 + // once it was resolved or rejected. So the first event that fires will resolve/reject the promise and the 471 + // remaining event won't be able to change that. The remaining event listener could be called later but it won't 472 + // have any effect. 473 + $(document).one('xwiki:document:saved', '.xcontent.form', resolve.bind(null, xwikiDocument)); 474 + $(document).one('xwiki:document:saveFailed', '.xcontent.form', reject.bind(null, xwikiDocument)); 475 + }); 435 435 }; 436 436 437 437 var maybeContinueEditing = function(continueEditing, xwikiDocument) { ... ... @@ -452,9 +452,9 @@ 452 452 453 453 // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML 454 454 // both if we stop editing now and if we continue but cancel the edit later. 455 - return xwikiDocument.reload().then( $.proxy(render,null, true)).then(456 - $.proxy(afterReloadAndRender,null, /* success: */ true),457 - $.proxy(afterReloadAndRender,null, /* success: */ false)496 + return xwikiDocument.reload().then(render.bind(null, true)).then( 497 + afterReloadAndRender.bind(null, /* success: */ true), 498 + afterReloadAndRender.bind(null, /* success: */ false) 458 458 ); 459 459 }; 460 460 ... ... @@ -473,25 +473,25 @@ 473 473 }; 474 474 475 475 // Make sure we unlock the document when the user navigates to another page. 476 - $(window).on('unload pagehide', $.proxy(unlock,null, currentXWikiDocument));517 + $(window).on('unload pagehide', unlock.bind(null, currentXWikiDocument)); 477 477 478 478 var shouldReload = function(xwikiDocument) { 479 479 var reloadEventFired = false; 480 - $(document).one('xwiki:actions:reload.maybe', function() { 521 + $(document).one('xwiki:actions:reload.maybe', '.xcontent.form', function() { 481 481 reloadEventFired = true; 482 482 }); 483 - vardeferred= $.Deferred();484 - // Wait a bit to see if the reload event is fired. 485 - setTimeout(function() { 486 - // Remove the listener in case the reload event wasn't fired. 487 - $(document).off('xwiki:actions:reload.maybe'); 488 - if (reloadEventFired) { 489 - deferred.resolve(xwikiDocument);490 - } else { 491 - deferred.reject(xwikiDocument);492 - } 493 - }, 0); 494 - return deferred.promise();524 + return new Promise((resolve, reject) => { 525 + // Wait a bit to see if the reload event is fired. 526 + setTimeout(function() { 527 + // Remove the listener in case the reload event wasn't fired. 528 + $(document).off('xwiki:actions:reload.maybe'); 529 + if (reloadEventFired) { 530 + resolve(xwikiDocument); 531 + } else { 532 + reject(xwikiDocument); 533 + } 534 + }, 0); 535 + }); 495 495 }; 496 496 497 497 var reload = function(xwikiDocument) { ... ... @@ -500,21 +500,33 @@ 500 500 }; 501 501 502 502 var view = function(xwikiDocument, reload) { 544 + var viewContent = $('#xwikicontent'); 503 503 // Destroy the editors before returning to view. 504 - $(document).trigger('xwiki:actions:view', {document: xwikiDocument});546 + viewContent.trigger('xwiki:actions:view', {document: xwikiDocument}); 505 505 $('#document-title h1').html(xwikiDocument.renderedTitle); 506 - $('#xwikicontent').html(xwikiDocument.renderedContent);548 + viewContent.html(xwikiDocument.renderedContent); 507 507 if (!reload) { 508 508 // If the user has canceled the edit then the restored page content may include the section edit links. Show them 509 509 // in case they were hidden. 510 - $('#xwikicontent').children(':header').children('.edit_section').removeClass('hidden');552 + viewContent.children(':header').children('.edit_section').removeClass('hidden'); 511 511 // Let others know that the DOM has been updated, in order to enhance it. 512 - $(document).trigger('xwiki:dom:updated', {'elements': $('#xwikicontent').toArray()});554 + $(document).trigger('xwiki:dom:updated', {'elements': viewContent.toArray()}); 513 513 } 514 - return $.Deferred().resolve(xwikiDocument).promise(); 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); 515 515 }; 516 516 517 517 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'); 518 518 return initActionButtons(xwikiDocument).then(initTitleEditor).then(initContentEditor) 519 519 .then(startRealTimeEditingSession); 520 520 }; ... ... @@ -521,7 +521,7 @@ 521 521 522 522 var initActionButtons = function(xwikiDocument) { 523 523 if (xwikiDocument) { 524 - maybeShowTranslateButton(xwikiDocument);577 + initTranslateButton(xwikiDocument); 525 525 } 526 526 var editContent = $('#xwikicontent'); 527 527 // We need the wrapper because #xwikicontent uses Bootstrap grid (col-xs-12) which is implemented with CSS float. ... ... @@ -528,178 +528,173 @@ 528 528 // Thus we need to use the grid for the sticky buttons also otherwise the postion is badly computed when scrolling 529 529 // (because of the float on the previous element). This wouldn't be needed if we were using position:sticky, which 530 530 // we can't use yet because it's not implemented on IE11 which we still have to support. 531 - var actionButtonsWrapper = editContent.nextAll('.sticky-buttons-wrapper'); 532 - if (actionButtonsWrapper.length === 0) { 533 - actionButtonsWrapper = $('<div class="sticky-buttons-wrapper col-xs-12">' + 534 - '<div class="inplace-editing-buttons sticky-buttons"/></div>').insertAfter(editContent); 535 - var actionButtons = actionButtonsWrapper.children('.sticky-buttons').data('xwikiDocument', xwikiDocument) 536 - .toggle(!!xwikiDocument); 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); 537 537 return loadActionButtons(actionButtons); 538 538 } else { 539 539 // If we're editing a page.. 540 540 if (xwikiDocument) { 541 541 // ..then make sure the action buttons are displayed right away (don't wait for the user to scroll). 542 - actionButtonsWrapper.children('.sticky-buttons').data('xwikiDocument', xwikiDocument).show(); 543 - // The action buttons are disabled on Save & View. We don't reload the page on Save & View and we reuse the 544 - // action buttons so we need to re-enable them each time we enter the edit mode. 545 - fakeForm.enable(); 546 - $(document).trigger('xwiki:dom:refresh'); 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(); 547 547 } 548 - return $.Deferred().resolve(xwikiDocument).promise();620 + return Promise.resolve(xwikiDocument); 549 549 } 550 550 }; 551 551 552 - var maybeShowTranslateButton = function(xwikiDocument) { 553 - var xwikiDocumentLocale = xwikiDocument.getRealLocale(); 554 - var uiLocale = $('html').attr('lang'); 555 - if (xwikiDocumentLocale && xwikiDocumentLocale !== uiLocale) { 556 - $('#tmTranslate').off('click.translate').on('click.translate', function(event) { 557 - event.preventDefault(); 558 - $(this).addClass('hidden'); 559 - xwikiDocument.language = uiLocale; 560 - // Update the document translation fields that are not 'shared' with the original document. 561 - xwikiDocument.isNew = true; 562 - delete xwikiDocument.version; 563 - delete xwikiDocument.majorVersion; 564 - delete xwikiDocument.minorVersion; 565 - $('#document-title-input').focus().select(); 566 - var message = $jsontool.serialize($services.localization.render('edit.inplace.page.translation', 567 - ['__locale__'])); 568 - new XWiki.widgets.Notification( 569 - message.replace('__locale__', uiLocale), 570 - 'info' 571 - ); 572 - }).removeClass('hidden'); 573 - var message = $jsontool.serialize($services.localization.render('edit.inplace.page.original', ['__locale__'])); 574 - new XWiki.widgets.Notification( 575 - message.replace('__locale__', xwikiDocumentLocale), 576 - 'info' 577 - ); 578 - } 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 + }); 579 579 }; 580 580 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 + 581 581 var loadActionButtons = function(actionButtons) { 582 - $(document).on('xwiki:actions:view', function() { 660 + // We want to update the form data as late as possible (but still before the form is validated), in order to allow 661 + // the title and content editors to update their values and the 'xwikiDocument' instance. We do this by catching the 662 + // event early (lower in the DOM, at the start of the event bubbling phase) and adding a one time event listener for 663 + // the end of the event bubbling phase at the top level of the DOM document. 664 + actionButtons.on('xwiki:actions:beforeSave', function() { 665 + $(document).one('xwiki:actions:beforeSave', updateFormDataBeforeSave); 666 + }); 667 + actionButtons.on('xwiki:actions:cancel', function(event) { 668 + // We are already in view mode so there's no need to leave the page. 669 + event.preventDefault(); 670 + }); 671 + $(document).on('xwiki:actions:view', '.xcontent.form', function(event, data) { 583 583 // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit 584 584 // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing 585 585 // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action 586 586 // buttons to have them ready for the next editing session (the user can save or cancel and then edit again 587 587 // without reloading the page). 588 - actionButtons.find(':input').blur().prop('disabled', true).end().hide(); 589 - // Hide the translate button because it can be used only in edit mode for the moment. 590 - $('#tmTranslate').addClass('hidden'); 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 + } 591 591 }); 592 - return $.get(XWiki.currentDocument.getURL('get'), { 692 + return Promise.resolve($.get(XWiki.currentDocument.getURL('get'), { 593 593 xpage: 'editactions' 594 - }).then( function(html){694 + })).then(html => { 595 595 actionButtons.html(html); 596 596 // Fix the name of the Save & View action. 597 597 actionButtons.find('.btn-primary').first().attr('name', 'action_save'); 598 - // Append the hidden input field that keeps the CSRF token. 599 - $('<input type="hidden" name="form_token" />').val(xcontext.form_token).appendTo(actionButtons); 600 - // We need a place where actionButtons.js can add more hidden inputs. 601 - actionButtons.append('<div class="hidden extra"/>'); 602 - var deferred = $.Deferred(); 603 - require(['actionButtons', 'xwiki-diff', 'autoSave'], function() { 604 - overrideEditActions(); 605 - overrideAjaxSaveAndContinue(); 606 - // Activate the auto-save feature passing our fake edit form. Note that autosave.js also creates an instance of 607 - // AutoSave but it doesn't do anything because it doesn't find a real edit form in the page. This is why we have 608 - // to create our own instance of AutoSave passing the right (fake) form. 609 - new XWiki.editors.AutoSave({form: fakeForm}); 610 - var xwikiDocument = actionButtons.data('xwikiDocument'); 611 - // Enable the action buttons (and their shortcut keys) only if we're editing a document. 612 - actionButtons.find(':input').prop('disabled', !xwikiDocument); 613 - deferred.resolve(xwikiDocument); 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 + }); 614 614 }); 615 - return deferred.promise(); 616 - }, function() { 617 - new XWiki.widgets.Notification( 618 - $jsontool.serialize($services.localization.render('edit.inplace.actionButtons.loadFailed')), 619 - 'error' 620 - ); 709 + }).catch(() => { 710 + new XWiki.widgets.Notification(l10n['edit.inplace.actionButtons.loadFailed'], 'error'); 621 621 }); 622 622 }; 623 623 624 - // actionButtons.js expects a form so we use a fake form. Refactoring actionButtons.js is too dangerous ATM. 625 - var fakeForm = { 626 - action: XWiki.currentDocument.getURL('save'), 627 - async: true, 628 - _getActionButtons: function() { 629 - if (!this._actionButtons) { 630 - this._actionButtons = $('#xwikicontent').nextAll('.sticky-buttons-wrapper').children('.sticky-buttons'); 631 - } 632 - return this._actionButtons; 633 - }, 634 - disable: function() { 635 - this._getActionButtons().find(':input').prop('disabled', true); 636 - }, 637 - enable: function() { 638 - // Clear the extra hidden input fields, that actionButtons.js might have added, each time the form is (re)enabled 639 - // (i.e. after a failed Save & View or before entering the edit mode) because they are designed to be used once. 640 - this._getActionButtons().find('.hidden.extra').empty(); 641 - this._getActionButtons().find(':input').prop('disabled', false); 642 - }, 643 - insert: function(element) { 644 - this._getActionButtons().find('.hidden.extra').append(element); 645 - }, 646 - down: function(selector) { 647 - return this._getActionButtons().find(selector)[0]; 648 - }, 649 - serialize: function() { 650 - var extra = this._getActionButtons().find(':input').serializeArray().reduce(function(extra, entry) { 651 - var value = extra[entry.name] || []; 652 - value.push(entry.value); 653 - extra[entry.name] = value; 654 - return extra; 655 - }, {}); 656 - var xwikiDocument = this._getActionButtons().data('xwikiDocument'); 657 - var formData = { 658 - title: xwikiDocument.rawTitle, 659 - language: xwikiDocument.getRealLocale(), 660 - isNew: xwikiDocument.isNew 661 - }; 662 - if (xwikiDocument.content != xwikiDocument.originalDocument.content) { 663 - // Submit the raw (source) content. No syntax conversion is needed in this case. 664 - formData.content = xwikiDocument.content; 665 - } else { 666 - // Submit the rendered content (HTML), but make sure it is converted to the document syntax on the server. 667 - $.extend(formData, { 668 - content: xwikiDocument.renderedContent, 669 - RequiresHTMLConversion: 'content', 670 - content_syntax: xwikiDocument.syntax 671 - }); 672 - } 673 - // Check for merge conflicts only if the document is not new and we know the current version. 674 - if (!xwikiDocument.isNew && xwikiDocument.version) { 675 - formData.previousVersion = xwikiDocument.version; 676 - // It would have been easier to send the timestamp but that's what the Save action expects. 677 - formData.editingVersionDate = new Date(xwikiDocument.modified).toISOString(); 678 - } 679 - return $.extend(formData, extra); 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()); 680 680 } 681 681 }; 682 682 683 - var overrideEditActions = function() { 684 - // Override the EditActions.notify() function in order to pass a fake form in the event parameters. 685 - var originalNotify = XWiki.actionButtons.EditActions.prototype.notify; 686 - XWiki.actionButtons.EditActions.prototype.notify = function(originalEvent, action, params) { 687 - if (params && $(originalEvent.element()).closest('.inplace-editing-buttons').length > 0) { 688 - // actionButtons.js expects a form so we use a fake form. Refactoring actionButtons.js is too dangerous ATM. 689 - // Note that we do this only when the event has parameters because we want to exclude the cancel event for which 690 - // actionButtons.js changes the window location if a form is specified, and we want to prevent that. 691 - params.form = fakeForm; 692 - } 693 - return originalNotify.apply(this, arguments); 694 - }; 695 - }; 696 - 697 697 var overrideAjaxSaveAndContinue = function() { 698 698 var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype); 699 699 $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, { 700 700 reloadEditor: function() { 701 - if ($('.inplace-editing-buttons').is(':visible')) { 702 - $(document).trigger('xwiki:actions:reload'); 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); 703 703 } else { 704 704 return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments); 705 705 } ... ... @@ -720,18 +720,26 @@ 720 720 721 721 var initTitleEditor = function(xwikiDocument) { 722 722 var label = $('<label for="document-title-input" class="sr-only"/>') 723 - .text($jsontool.serialize($services.localization.render('core.editors.content.titleField.label'))); 724 - var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.rawTitle); 725 - var placeholder = xwikiDocument.documentReference.name; 726 - if (placeholder === 'WebHome') { 727 - placeholder = xwikiDocument.documentReference.parent.name; 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); 728 728 } 729 - input.attr('placeholder', placeholder); 730 730 $('#document-title h1').addClass('editable').empty().append([label, input]); 731 - $(document).on('xwiki:actions:beforeSave.titleEditor', function(event) { 787 + $(document).on('xwiki:actions:beforeSave.titleEditor', '.xcontent.form', function(event) { 732 732 xwikiDocument.rawTitle = input.val(); 733 733 }); 734 - $(document).one('xwiki:actions:view', function(event, data) { 790 + $(document).one('xwiki:actions:view', '.xcontent.form', function(event, data) { 735 735 // Destroy the title editor. 736 736 $(document).off('xwiki:actions:beforeSave.titleEditor'); 737 737 $('#document-title h1').removeClass('editable').text(xwikiDocument.rawTitle); ... ... @@ -751,28 +751,25 @@ 751 751 // Keep the focus while the edit content is being prepared. 752 752 viewContent.focus(); 753 753 } 754 - var data = { 755 - contentType: 'org.xwiki.rendering.syntax.SyntaxContent', 756 - editMode: 'wysiwyg', 810 + var data = $.extend({}, config, { 757 757 document: xwikiDocument, 758 758 // The content editor is loaded on demand, asynchronously. 759 - deferred: $.Deferred(), 760 - // We have to explicitly enable the source mode for in-line edit because the latest version of the content editor 761 - // could be installed on an older version of XWiki where the in-place editor didn't support the source mode (so 762 - // the content editor cannot enable the source mode by default). 763 - enableSourceMode: true 764 - }; 765 - var editContentPromise = data.deferred.promise(); 766 - editContentPromise.done(function() { 813 + deferred: $.Deferred() 814 + }); 815 + editContent.trigger('xwiki:actions:edit', data); 816 + return data.deferred.promise().then(() => { 767 767 editContent.show(); 768 768 viewContent.remove(); 769 769 if (withFocus) { 770 - // Restore the focus when the edit content is ready but make sure we don't scroll the page. 771 - editContent[0].focus({preventScroll: true}); 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); 772 772 } 827 + return xwikiDocument; 773 773 }); 774 - editContent.trigger('xwiki:actions:edit', data); 775 - return editContentPromise; 776 776 }; 777 777 778 778 var startRealTimeEditingSession = function(xwikiDocument) { ... ... @@ -791,9 +791,10 @@ 791 791 }; 792 792 793 793 return { 794 - preload: preload, 795 - editPage: editPage, 796 - editSection: editSection 847 + preload, 848 + editPage, 849 + editSection, 850 + translatePage 797 797 }; 798 798 }); 799 799 ... ... @@ -803,8 +803,7 @@ 803 803 return; 804 804 } 805 805 806 - var inplaceEditingConfig = $('div[data-inplace-editing-config]').data('inplaceEditingConfig') || {}; 807 - var wysiwygEditorModule = 'xwiki-' + inplaceEditingConfig.wysiwygEditor + '-inline'; 860 + var wysiwygEditorModule = 'xwiki-' + config.wysiwygEditor + '-inline'; 808 808 809 809 var preloadEditor = function() { 810 810 require(['editInPlace', wysiwygEditorModule], function(editInPlace) { ... ... @@ -822,44 +822,193 @@ 822 822 }); 823 823 } 824 824 825 - var editButton = $('#tmEdit > a'); 826 - editButton.on('click.inPlaceEditing', function(event) { 827 - event.preventDefault(); 878 + var onInPlaceEditing = function(event) { 828 828 // Make sure the user doesn't try to re-activate the edit mode while we are in edit mode. 829 - editButton.addClass('disabled'); 830 - // Load the code needed to edit in place only when the edit button is clicked. 831 - require(['editInPlace', wysiwygEditorModule], function(editInPlace) { 832 - editInPlace.editPage().always(function() { 833 - editButton.removeClass('disabled'); 834 - }); 835 - // Fallback on the standalone edit mode if we fail to load the required modules. 836 - }, $.proxy(disableInPlaceEditing, event.target)); 837 - }); 838 - 839 - // Section in-place editing. 840 - $('#xwikicontent').on('click.inPlaceEditing', '> :header > a.edit_section:not(.disabled)', function(event) { 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'); 841 841 event.preventDefault(); 842 - // Make sure the user doesn't try to re-activate the edit mode while we are in edit mode. 843 - editButton.addClass('disabled'); 844 - // Hide the section editing links and focus the content right away. We could have replaced the section editing icon 845 - // with a loading animation / spinner but giving instant visual feedback about what is going to happen is perceived 846 - // better by the users (it feels faster). 847 - $('#xwikicontent').attr('tabindex', '0').focus().children(':header').children('.edit_section').addClass('hidden'); 848 - var heading = $(event.target).closest(':header'); 891 + const handler = event.data; 892 + const data = handler.beforeEdit?.(event); 849 849 // Load the code needed to edit in place only when the edit button is clicked. 850 850 require(['editInPlace', wysiwygEditorModule], function(editInPlace) { 851 - editInPlace.editSection(heading.attr('id')).always(function() { 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 852 852 editButton.removeClass('disabled'); 902 + editButton.removeAttr('aria-disabled'); 903 + editButton.removeAttr('role'); 904 + editButton.attr('href', reference); 853 853 }); 854 854 // Fallback on the standalone edit mode if we fail to load the required modules. 855 - }, $.proxy(disableInPlaceEditing,event.target));856 - } );907 + }, disableInPlaceEditing.bind(event.target)); 908 + }; 857 857 858 858 var disableInPlaceEditing = function() { 859 - editButton.off('click.inPlaceEditing').removeClass('disabled'); 911 + editButton.add(translateButton).off('click.inPlaceEditing').removeClass('disabled'); 860 860 $('#xwikicontent').off('click.inPlaceEditing').removeAttr('tabindex').children(':header').children('.edit_section') 861 861 .removeClass('hidden'); 862 862 // Fallback on the standalone edit mode. 863 863 $(this).click(); 864 864 }; 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 + } 865 865 }); 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 + {}); - Analizza il contenuto
-
... ... @@ -1,1 +1,1 @@ 1 - Sì1 +No
- XWiki.StyleSheetExtension[0]
-
- Codice
-
... ... @@ -9,10 +9,9 @@ 9 9 @document-title-input-padding-vertical: @line-height-computed / 4 - 1; 10 10 input#document-title-input { 11 11 /* Preserve the heading styles. */ 12 - border: 1px solid transparent; 13 - box-shadow: none; 14 14 color: inherit; 15 15 font-size: inherit; 14 + background-color: @body-bg; 16 16 /* It seems it's not enough to set the line height for the text input. We also need to set its height. */ 17 17 height: @font-size-document-title * @headings-line-height + 2 * (1 + @document-title-input-padding-vertical); 18 18 line-height: @headings-line-height; ... ... @@ -19,12 +19,16 @@ 19 19 padding: @document-title-input-padding-vertical (ceil(@grid-gutter-width / 2) - 1); 20 20 width: 100%; 21 21 } 21 +input#document-title-input:valid { 22 + border: 1px solid transparent; 23 + box-shadow: none; 24 +} 22 22 23 -input#document-title-input:hover { 26 +input#document-title-input:valid:hover { 24 24 border-color: @input-border; 25 25 } 26 26 27 -input#document-title-input:focus, 30 +input#document-title-input:valid:focus, 28 28 #xwikicontent[contenteditable]:focus, 29 29 #xwikicontent[tabindex]:focus { 30 30 .form-control-focus(); ... ... @@ -50,7 +50,7 @@ 50 50 padding-top: @line-height-computed * 0.75; 51 51 } 52 52 53 - .sticky-buttons-wrapper{56 +form#inplace-editing { 54 54 /* Leave some space for the bottom box shadow of the editing area. */ 55 55 margin-top: 7px; 56 56 }
- XWiki.UIExtensionClass[0]
-
- Executed Content
-
... ... @@ -1,3 +1,11 @@ 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 3 #if ($services.edit.document.inPlaceEditingEnabled() && $hasEdit && $xcontext.action == 'view' && !$doc.isNew()) ... ... @@ -4,10 +4,58 @@ 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 + '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). 7 7 #set ($inplaceEditingConfig = { 46 + 'contentType': 'org.xwiki.rendering.syntax.SyntaxContent', 8 8 'editMode': $defaultEditMode, 9 - 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id 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 10 10 }) 66 + #set ($inplaceEditingConfig.titleIsMandatory = $xwiki.getSpacePreference('xwiki.title.mandatory') == 1) 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. 13 13 #set ($discard = $xwiki.ssx.use('XWiki.InplaceEditing'))
- 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 ($services.edit.document.inPlaceEditingEnabled() && $hasEdit && $xwiki.isMultiLingual() 9 - && $tdoc.realLocale == $doc.realLocale && "$!doc.realLocale" != '' && $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