Changes for page InplaceEditing

From version 6.1
edited by Nazzareno Pompei
on 19/01/2024 08:57
Change comment: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/15.10.5]
To version 2.1
edited by N Pompei
on 16/09/2020 12:29
Change comment: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/12.7.1]

Summary

Details

Page properties
Author
... ... @@ -1,1 +1,1 @@
1 -XWiki.NazzarenoPompei
1 +XWiki.NPompei
Content
... ... @@ -9,15 +9,11 @@
9 9   #jsonResponse($editConfirmation)
10 10   #else
11 11   ## Lock the document for editing.
12 - #set ($lockParams = {
12 + #set ($discard = $response.sendRedirect($tdoc.getURL('lock', $escapetool.url({
13 13   'ajax': 1,
14 14   'action': $request.lockAction,
15 15   'language': $tdoc.realLocale
16 - })
17 - #if ($request.force == 'true')
18 - #set ($lockParams.force = 1)
19 - #end
20 - #set ($discard = $response.sendRedirect($tdoc.getURL('lock', $escapetool.url($lockParams))))
16 + }))))
21 21   #end
22 22  #end
23 23  {{/velocity}}
XWiki.JavaScriptExtension[0]
Code
... ... @@ -1,11 +7,10 @@
1 -(function(config) {
2 - "use strict";
3 -
4 -const paths = config.paths;
5 -const l10n = config.l10n;
6 -
7 7  require.config({
8 - paths: paths.js
2 + paths: {
3 + 'actionButtons': $jsontool.serialize($xwiki.getSkinFile('js/xwiki/actionbuttons/actionButtons.js', true)),
4 + '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 + }
9 9  });
10 10  
11 11  define('xwiki-document-api', ['jquery'], function($) {
... ... @@ -16,36 +16,23 @@
16 16  
17 17   return {
18 18   /**
19 - * @return this document's plain title
20 - */
21 - getPlainTitle() {
22 - return $('<div/>').html(this.renderedTitle || this.title).text();
23 - },
24 -
25 - /**
26 26   * @return this document's real locale
27 27   */
28 28   getRealLocale: function() {
29 29   var realLocale = this.language;
30 - if (typeof realLocale !== 'string' || realLocale === '') {
31 - realLocale = this.getDefaultLocale();
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');
32 32   }
33 33   return realLocale;
34 34   },
35 35  
36 36   /**
37 - * @return this document's default locale
38 - */
39 - getDefaultLocale: function() {
40 - if (this.translations && typeof this.translations['default'] === 'string') {
41 - return this.translations['default'];
42 - } else {
43 - // The default locale is not specified. Use the UI locale.
44 - return $('html').attr('lang');
45 - }
46 - },
47 -
48 - /**
49 49   * @return the URL that can be used to perform the specified action on this document
50 50   */
51 51   getURL: function(action, queryString, fragment) {
... ... @@ -82,29 +82,27 @@
82 82   var queryString = {
83 83   xpage: 'get',
84 84   outputTitle: true,
71 + outputSyntax: forView ? null : 'annotatedxhtml',
85 85   language: this.getRealLocale(),
86 86   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
87 87   timestamp: new Date().getTime()
88 88   };
89 - if (!forView) {
90 - // We need the annotated HTML when editing in order to be able to protect the rendering transformations and to
91 - // be able to recreate the wiki syntax.
92 - queryString.outputSyntax = 'annotatedhtml';
93 - queryString.outputSyntaxVersion = '5.0'
94 - // Currently, only the macro transformations are protected and thus can be edited.
95 - // See XRENDERING-78: Add markers to modified XDOM by Transformations/Macros
96 - queryString.transformations = 'macro';
97 - }
98 - return Promise.resolve($.get(this.getURL('view'), queryString)).then(html => {
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) {
99 99   // Render succeeded.
100 100   var container = $('<div/>').html(html);
101 - return $.extend(this, {
85 + return $.extend(thisXWikiDocument, {
102 102   renderedTitle: container.find('#document-title h1').html(),
103 103   renderedContent: container.find('#xwikicontent').html()
104 104   });
105 - }).catch(() => {
106 - new XWiki.widgets.Notification(l10n['edit.inplace.page.renderFailed'], 'error');
107 - return Promise.reject(this);
89 + }, function() {
90 + // Render failed.
91 + return thisXWikiDocument;
108 108   });
109 109   },
110 110  
... ... @@ -114,19 +114,20 @@
114 114   * @return a promise that resolves to this document instance if the reload request succeeds
115 115   */
116 116   reload: function() {
117 - return Promise.resolve($.getJSON(this.getRestURL(), {
101 + var thisXWikiDocument = this;
102 + return $.getJSON(this.getRestURL(), {
118 118   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
119 119   timestamp: new Date().getTime()
120 - })).then(newXWikiDocument => {
105 + }).then(function(newXWikiDocument) {
121 121   // Reload succeeded.
122 122   // Resolve the document reference.
123 - this.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);
108 + thisXWikiDocument.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT);
124 124   // We were able to load the document so it's not new.
125 - this.isNew = false;
126 - return $.extend(this, newXWikiDocument);
127 - }).catch(() => {
110 + thisXWikiDocument.isNew = false;
111 + return $.extend(thisXWikiDocument, newXWikiDocument);
112 + }, function() {
128 128   // Reload failed.
129 - return Promise.reject(this);
114 + return thisXWikiDocument;
130 130   });
131 131   },
132 132  
... ... @@ -137,8 +137,9 @@
137 137   * @return a promise that resolves to this document instance if the lock request succeeds
138 138   */
139 139   lock: function(action, force) {
125 + var thisXWikiDocument = this;
140 140   action = action || 'edit';
141 - return Promise.resolve($.getJSON(this.getURL('get'), {
127 + return $.getJSON(this.getURL('get'), {
142 142   sheet: 'XWiki.InplaceEditing',
143 143   action: 'lock',
144 144   lockAction: action,
... ... @@ -147,20 +147,20 @@
147 147   outputSyntax: 'plain',
148 148   // Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers).
149 149   timestamp: new Date().getTime()
150 - })).then(() => {
136 + }).then(function() {
151 151   // Lock succeeded.
152 - this.locked = action;
153 - return this;
154 - }).catch(response => {
138 + thisXWikiDocument.locked = action;
139 + return thisXWikiDocument;
140 + }, function(response) {
155 155   // Lock failed.
156 - delete this.locked;
142 + delete thisXWikiDocument.locked;
157 157   // Check if the user can force the lock.
158 158   var lockConfirmation = response.responseJSON;
159 159   if (response.status === 423 && lockConfirmation) {
160 160   // The user can force the lock, but needs confirmation.
161 - this.lockConfirmation = lockConfirmation;
147 + thisXWikiDocument.lockConfirmation = lockConfirmation;
162 162   }
163 - return Promise.reject(this);
149 + return thisXWikiDocument;
164 164   });
165 165   },
166 166  
... ... @@ -183,25 +183,6 @@
183 183   // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests
184 184   $.ajax({type: 'GET', url: url, async: false});
185 185   }
186 - },
187 -
188 - /**
189 - * Makes sure this document matches the current UI locale.
190 - */
191 - translate: function() {
192 - const realLocale = this.getRealLocale();
193 - const uiLocale = $('html').attr('lang');
194 - if (realLocale && realLocale !== uiLocale) {
195 - this.language = uiLocale;
196 - // Set the original document locale.
197 - this.translations = this.translations || {};
198 - this.translations['default'] = realLocale;
199 - // Update the document fields that are not 'shared' with the original document.
200 - this.isNew = true;
201 - delete this.version;
202 - delete this.majorVersion;
203 - delete this.minorVersion;
204 - }
205 205   }
206 206   };
207 207  });
... ... @@ -218,12 +218,15 @@
218 218   'xwiki-events-bridge'
219 219  ], function($, xcontext, xwikiDocumentAPI) {
220 220   var preload = function() {
221 - paths.css.forEach(loadCSS);
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)));
222 222   return initActionButtons();
223 223   };
224 224  
225 225   var loadCSS = function(url) {
226 - $('<link/>').attr({
196 + var link = $('<link>').attr({
227 227   type: 'text/css',
228 228   rel: 'stylesheet',
229 229   href: url
... ... @@ -238,12 +238,6 @@
238 238   });
239 239   };
240 240  
241 - var translatePage = function() {
242 - return editInPlace({
243 - afterEdit: createTranslation
244 - });
245 - };
246 -
247 247   var editSection = function(sectionId) {
248 248   return editInPlace({
249 249   lockFailed: function() {
... ... @@ -255,7 +255,7 @@
255 255   $('#xwikicontent').removeAttr('tabindex');
256 256   if (sectionId) {
257 257   // Select the heading of the specified section.
258 - $('#xwikicontent > #' + $.escapeSelector(sectionId)).each(function() {
222 + $('#xwikicontent > #' + escapeSelector(sectionId)).each(function() {
259 259   selectText(this);
260 260   });
261 261   }
... ... @@ -263,6 +263,22 @@
263 263   });
264 264   };
265 265  
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 +
266 266   // We preserve the document data between edits in order to be able to know which document translation should be edited
267 267   // (e.g. when the document translation is missing and we create it, the next edit session should target the created
268 268   // translation).
... ... @@ -270,11 +270,6 @@
270 270   language: xcontext.locale
271 271   }, xwikiDocumentAPI);
272 272  
273 - var setCurrentXWikiDocument = function(xwikiDocument) {
274 - currentXWikiDocument = xwikiDocument;
275 - return Promise.resolve(xwikiDocument);
276 - };
277 -
278 278   var editInPlace = function(options) {
279 279   options = $.extend({
280 280   afterEdit: function() {},
... ... @@ -282,57 +282,42 @@
282 282   }, options);
283 283   $('#xwikicontent').addClass('loading');
284 284   // Lock the document first.
285 - return lock(currentXWikiDocument)
260 + return lock(currentXWikiDocument).fail(options.lockFailed)
286 286   // Then load the document only if we managed to lock it.
287 - .then(load, xwikiDocument => {
288 - options.lockFailed(xwikiDocument);
289 - return Promise.reject(xwikiDocument);
262 + .then(load)
290 290   // Then load the editors only if we managed to load the document.
291 - }).then(edit).then(xwikiDocument => {
292 - options.afterEdit(xwikiDocument);
293 - return xwikiDocument;
294 - }).finally(() => {
295 - // Remove the aria-expanded attribute which is incorrect for role=textbox
296 - $('#xwikicontent').removeClass('loading').removeAttr('aria-expanded');
264 + .then(edit).done(options.afterEdit).always(function() {
265 + $('#xwikicontent').removeClass('loading');
297 297   // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly.
298 298   }).then(maybeSave)
299 299   // Then unlock the document both when the edit ended with success and with a failure.
300 300   .then(unlock, unlock)
301 301   // Finally view the document both when the edit ended with success and with a failure.
302 - .then(view, view)
303 - // Update the current document for the next edit session.
304 - .then(setCurrentXWikiDocument, setCurrentXWikiDocument);
271 + .then(view, view);
305 305   };
306 306  
307 307   var lock = function(xwikiDocument) {
308 - return xwikiDocument.lock().catch(function(xwikiDocument) {
275 + return xwikiDocument.lock().then(null, function(xwikiDocument) {
309 309   // If the document was already locked then we need to ask the user if they want to force the lock.
310 310   if (xwikiDocument.lockConfirmation) {
311 311   var confirmation = xwikiDocument.lockConfirmation;
312 312   delete xwikiDocument.lockConfirmation;
313 - return maybeForceLock(confirmation).then(xwikiDocument.lock.bind(xwikiDocument, 'edit', true), function() {
314 - // Cancel the edit action.
315 - return Promise.reject(xwikiDocument);
316 - });
280 + return maybeForceLock(confirmation).then($.proxy(xwikiDocument, 'lock', 'edit', true));
317 317   } else {
318 - new XWiki.widgets.Notification(l10n['edit.inplace.page.lockFailed'], 'error');
319 - return Promise.reject(xwikiDocument);
282 + new XWiki.widgets.Notification(
283 + $jsontool.serialize($services.localization.render('edit.inplace.page.lockFailed')),
284 + 'error'
285 + );
286 + return xwikiDocument;
320 320   }
321 321   });
322 322   };
323 323  
324 324   var maybeForceLock = function(confirmation) {
325 - var deferred, promise = new Promise((resolve, reject) => {
326 - deferred = {resolve, reject};
327 - });
328 - // We need the catch() to prevent the "Uncaught (in promise)" error log in the console.
329 - promise.catch(() => {}).finally(() => {
330 - // This flag is used by the Force Lock modal to know whether the promise is settled when the modal is closing.
331 - deferred.settled = true;
332 - });
292 + var deferred = $.Deferred();
333 333   // Reuse the confirmation modal once it is created.
334 334   var modal = $('.force-edit-lock-modal');
335 - if (!modal.length) {
295 + if (modal.length === 0) {
336 336   modal = createForceLockModal();
337 337   }
338 338   // Update the deferred that needs to be resolved or rejected.
... ... @@ -348,7 +348,7 @@
348 348   }
349 349   // Show the confirmation modal.
350 350   modal.modal('show');
351 - return promise;
311 + return deferred.promise();
352 352   };
353 353  
354 354   var createForceLockModal = function() {
... ... @@ -371,17 +371,17 @@
371 371   '</div>',
372 372   '</div>'
373 373   ].join(''));
374 - modal.find('.close').attr('aria-label', l10n['edit.inplace.close']);
375 - modal.find('.modal-footer .btn-warning').on('click', function() {
334 + modal.find('.close').attr('aria-label', $jsontool.serialize($services.localization.render('edit.inplace.close')));
335 + modal.find('.modal-footer .btn-warning').click(function() {
376 376   // The user has confirmed they want to force the lock.
377 377   modal.data('deferred').resolve();
378 378   modal.modal('hide');
379 379   });
380 380   modal.on('hide.bs.modal', function() {
381 - // If the lock promise is not yet settled when the modal is closing then it means the modal was canceled,
341 + // If the lock promise is not yet resolved when the modal is closing then it means the modal was canceled,
382 382   // i.e. the user doesn't want to force the lock.
383 383   var deferred = modal.data('deferred');
384 - if (!deferred.settled) {
344 + if (deferred.state() === 'pending') {
385 385   deferred.reject();
386 386   }
387 387   });
... ... @@ -389,19 +389,18 @@
389 389   };
390 390  
391 391   var load = function(xwikiDocument) {
392 - return xwikiDocument.reload().then(xwikiDocument => {
352 + return xwikiDocument.reload().done(function(xwikiDocument) {
393 393   // Clone the current document version and keep a reference to it in order to be able to restore it on cancel.
394 394   xwikiDocument.originalDocument = $.extend(true, {
395 395   renderedTitle: $('#document-title h1').html(),
396 396   renderedContent: $('#xwikicontent').html()
397 397   }, xwikiDocument);
398 - return xwikiDocument;
399 - }).catch(xwikiDocument => {
400 - new XWiki.widgets.Notification(l10n['edit.inplace.page.loadFailed'], 'error');
401 - return Promise.reject(xwikiDocument);
358 + }).fail(function() {
359 + new XWiki.widgets.Notification($jsontool.serialize($services.localization.render('edit.inplace.page.loadFailed')),
360 + 'error');
402 402   // Render the document for edit, in order to have the annotated content HTML. The annotations are used to protect
403 403   // the rendering transformations (e.g. macros) when editing the content.
404 - }).then(render.bind(null, false));
363 + }).then($.proxy(render, null, false));
405 405   };
406 406  
407 407   /**
... ... @@ -414,7 +414,7 @@
414 414   };
415 415  
416 416   var maybeSave = function(xwikiDocument) {
417 - return waitForAction(xwikiDocument).then(action => {
376 + return waitForAction(xwikiDocument).then(function(action) {
418 418   switch(action.name) {
419 419   case 'save': return save({
420 420   document: action.document,
... ... @@ -427,29 +427,29 @@
427 427   };
428 428  
429 429   var waitForAction = function(xwikiDocument) {
430 - return new Promise((resolve, reject) => {
431 - // We wait for the first save, reload or cancel event, whichever is triggered first. Note that the event listeners
432 - // that are not executed first will remain registered but that doesn't cause any problems because the state of a
433 - // deferred object (promise) cannot change once it was resolved. So the first event that fires will resolve the
434 - // promise and the remaining events won't be able to change that. The remaining event listeners could be called
435 - // later but they won't have any effect on the deferred object.
436 - $(document).one([
437 - 'xwiki:actions:save',
438 - 'xwiki:actions:reload',
439 - 'xwiki:actions:cancel',
440 - ].join(' '), '.xcontent.form', function(event, data) {
441 - resolve({
442 - name: event.type.substring('xwiki:actions:'.length),
443 - document: xwikiDocument,
444 - data: data
445 - });
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
446 446   });
447 447   });
406 + return deferred.promise();
448 448   };
449 449  
450 450   var save = function(data) {
451 451   // Push the changes to the server.
452 - return push(data.document).then(xwikiDocument => {
411 + return push(data.document).then(function(xwikiDocument) {
453 453   // Save succeeded.
454 454   return shouldReload(xwikiDocument).then(
455 455   // The document was saved with merge and thus if we want to continue eding we need to reload the editor (because
... ... @@ -456,7 +456,7 @@
456 456   // its content doesn't match the saved content).
457 457   reload,
458 458   // No need to reload the editor because either the action was Save & View or there was no merge on save.
459 - maybeContinueEditing.bind(null, data['continue'])
418 + $.proxy(maybeContinueEditing, null, data['continue'])
460 460   );
461 461   // Save failed. Continue editing because we may have unsaved content.
462 462   }, maybeSave);
... ... @@ -464,15 +464,15 @@
464 464  
465 465   var push = function(xwikiDocument) {
466 466   // Let actionButtons.js do the push. We just catch the result.
467 - 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 - });
426 + var deferred = $.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 remaining
430 + // event won't be able to change that. The remaining event listener could be called later but it won't have any
431 + // 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();
476 476   };
477 477  
478 478   var maybeContinueEditing = function(continueEditing, xwikiDocument) {
... ... @@ -493,9 +493,9 @@
493 493  
494 494   // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML
495 495   // both if we stop editing now and if we continue but cancel the edit later.
496 - return xwikiDocument.reload().then(render.bind(null, true)).then(
497 - afterReloadAndRender.bind(null, /* success: */ true),
498 - afterReloadAndRender.bind(null, /* success: */ false)
455 + return xwikiDocument.reload().then($.proxy(render, null, true)).then(
456 + $.proxy(afterReloadAndRender, null, /* success: */ true),
457 + $.proxy(afterReloadAndRender, null, /* success: */ false)
499 499   );
500 500   };
501 501  
... ... @@ -514,25 +514,25 @@
514 514   };
515 515  
516 516   // Make sure we unlock the document when the user navigates to another page.
517 - $(window).on('unload pagehide', unlock.bind(null, currentXWikiDocument));
476 + $(window).on('unload pagehide', $.proxy(unlock, null, currentXWikiDocument));
518 518  
519 519   var shouldReload = function(xwikiDocument) {
520 520   var reloadEventFired = false;
521 - $(document).one('xwiki:actions:reload.maybe', '.xcontent.form', function() {
480 + $(document).one('xwiki:actions:reload.maybe', function() {
522 522   reloadEventFired = true;
523 523   });
524 - return new Promise((resolve, reject) => {
525 - // Wait a bit to see if the reload event is fired.
526 - setTimeout(function() {
527 - // Remove the listener in case the reload event wasn't fired.
528 - $(document).off('xwiki:actions:reload.maybe');
529 - if (reloadEventFired) {
530 - resolve(xwikiDocument);
531 - } else {
532 - reject(xwikiDocument);
533 - }
534 - }, 0);
535 - });
483 + var deferred = $.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();
536 536   };
537 537  
538 538   var reload = function(xwikiDocument) {
... ... @@ -541,33 +541,21 @@
541 541   };
542 542  
543 543   var view = function(xwikiDocument, reload) {
544 - var viewContent = $('#xwikicontent');
545 545   // Destroy the editors before returning to view.
546 - viewContent.trigger('xwiki:actions:view', {document: xwikiDocument});
504 + $(document).trigger('xwiki:actions:view', {document: xwikiDocument});
547 547   $('#document-title h1').html(xwikiDocument.renderedTitle);
548 - viewContent.html(xwikiDocument.renderedContent);
506 + $('#xwikicontent').html(xwikiDocument.renderedContent);
549 549   if (!reload) {
550 550   // If the user has canceled the edit then the restored page content may include the section edit links. Show them
551 551   // in case they were hidden.
552 - viewContent.children(':header').children('.edit_section').removeClass('hidden');
510 + $('#xwikicontent').children(':header').children('.edit_section').removeClass('hidden');
553 553   // Let others know that the DOM has been updated, in order to enhance it.
554 - $(document).trigger('xwiki:dom:updated', {'elements': viewContent.toArray()});
512 + $(document).trigger('xwiki:dom:updated', {'elements': $('#xwikicontent').toArray()});
555 555   }
556 - // Remove the action events scope.
557 - viewContent.closest('.form').removeClass('form');
558 - // Update the URL.
559 - if (window.location.hash === '#edit' || window.location.hash === '#translate') {
560 - history.replaceState(null, null, '#');
561 - }
562 - return Promise.resolve(xwikiDocument);
514 + return $.Deferred().resolve(xwikiDocument).promise();
563 563   };
564 564  
565 565   var edit = function(xwikiDocument) {
566 - // By adding the 'form' CSS class we set the scope of the action events (e.g. xwiki:actions:beforeSave or
567 - // xwiki:actions:cancel). We need this because in view mode we can have multiple forms active on the page (e.g. one
568 - // for editing the document content in place and one for editing the document syntax in-place) and we don't want
569 - // them to interfere (e.g. canceling one form shouldn't cancel the other forms).
570 - $('#xwikicontent').closest('.xcontent').addClass('form');
571 571   return initActionButtons(xwikiDocument).then(initTitleEditor).then(initContentEditor)
572 572   .then(startRealTimeEditingSession);
573 573   };
... ... @@ -574,7 +574,7 @@
574 574  
575 575   var initActionButtons = function(xwikiDocument) {
576 576   if (xwikiDocument) {
577 - initTranslateButton(xwikiDocument);
524 + maybeShowTranslateButton(xwikiDocument);
578 578   }
579 579   var editContent = $('#xwikicontent');
580 580   // We need the wrapper because #xwikicontent uses Bootstrap grid (col-xs-12) which is implemented with CSS float.
... ... @@ -581,173 +581,178 @@
581 581   // Thus we need to use the grid for the sticky buttons also otherwise the postion is badly computed when scrolling
582 582   // (because of the float on the previous element). This wouldn't be needed if we were using position:sticky, which
583 583   // we can't use yet because it's not implemented on IE11 which we still have to support.
584 - let inplaceEditingForm = editContent.nextAll('form#inplace-editing');
585 - if (!inplaceEditingForm.length) {
586 - // The 'xwikieditcontent' id is needed for the auto-save feature (otherwise it doesn't find the form).
587 - inplaceEditingForm = $(`
588 - <form id="inplace-editing" class="col-xs-12">
589 - <div hidden>
590 - <input type="hidden" name="form_token" />
591 - <input type="hidden" name="async" value="true" />
592 - <input type="hidden" name="content" />
593 - <input type="hidden" name="RequiresHTMLConversion" value="content" />
594 - <input type="hidden" name="content_syntax" />
595 - <input type="hidden" name="language" />
596 - </div>
597 - <fieldset id="xwikieditcontent" class="xform inplace-editing-buttons sticky-buttons"></fieldset>
598 - </form>
599 - `).attr('action', XWiki.currentDocument.getURL('save'))
600 - .insertAfter(editContent).toggle(!!xwikiDocument);
601 - inplaceEditingForm.find('input[name="form_token"]').val(xcontext.form_token);
602 - var actionButtons = inplaceEditingForm.children('.sticky-buttons').data('xwikiDocument', xwikiDocument);
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);
603 603   return loadActionButtons(actionButtons);
604 604   } else {
605 605   // If we're editing a page..
606 606   if (xwikiDocument) {
607 607   // ..then make sure the action buttons are displayed right away (don't wait for the user to scroll).
608 - inplaceEditingForm.show().children('.sticky-buttons')
609 - .data('xwikiDocument', xwikiDocument)
610 - // Make sure the position of the action buttons is updated.
611 - .trigger('xwiki:dom:refresh')
612 - // The action buttons are disabled on Save & View. We don't reload the page on Save & View and we reuse the
613 - // action buttons so we need to re-enable them each time we enter the edit mode.
614 - .prop('disabled', false);
615 - // Cleanup the extra hidden input fields that actionButtons.js might have appended to the form. We have to do
616 - // this each time the form is (re)enabled (i.e. after a failed Save & View or before entering the edit mode)
617 - // because they are designed to be used once.
618 - inplaceEditingForm.children('fieldset').nextAll().remove();
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');
619 619   }
620 - return Promise.resolve(xwikiDocument);
548 + return $.Deferred().resolve(xwikiDocument).promise();
621 621   }
622 622   };
623 623  
624 - var createTranslation = function(xwikiDocument) {
625 - xwikiDocument.translate();
626 - $('#document-title-input').focus().select();
627 - // Let the user know that they are now editing the translation of this page in the current locale.
628 - $('#document-title-input').popover({
629 - content: l10n['edit.inplace.page.translate.messageAfter'],
630 - placement: 'bottom',
631 - trigger: 'manual'
632 - }).popover('show').one('blur', function() {
633 - // Hide the popover when the title input loses the focus.
634 - $(this).popover('hide');
635 - });
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 + }
636 636   };
637 637  
638 - var initTranslateButton = function(xwikiDocument) {
639 - // Initialize the translate button only if it's visible.
640 - const translateButton = $(config.translateButtonSelector).filter('[data-toggle="popover"]').filter(':visible');
641 - translateButton.off('click.translate').on('click.translate', function(event) {
642 - event.preventDefault();
643 - translateButton.parent().addClass('hidden');
644 - createTranslation(xwikiDocument);
645 - // Let the user know that they are editing the original version of the page and not the translation corresponding
646 - // to the current locale because there isn't one created yet.
647 - }).attr({
648 - // Backup the initial popover message to be able to restore it on view.
649 - 'data-content-view': translateButton.attr('data-content'),
650 - // Use a custom popover message dedicated to the edit action.
651 - 'data-content': l10n['edit.inplace.page.translate.messageBefore']
652 - }).popover('show')
653 - // Hide the popover on the next click. The user can still see the message by hovering the translate button.
654 - .closest('html').one('click', function() {
655 - translateButton.popover('hide');
656 - });
657 - };
658 -
659 659   var loadActionButtons = function(actionButtons) {
660 - // We want to update the form data as late as possible (but still before the form is validated), in order to allow
661 - // the title and content editors to update their values and the 'xwikiDocument' instance. We do this by catching the
662 - // event early (lower in the DOM, at the start of the event bubbling phase) and adding a one time event listener for
663 - // the end of the event bubbling phase at the top level of the DOM document.
664 - actionButtons.on('xwiki:actions:beforeSave', function() {
665 - $(document).one('xwiki:actions:beforeSave', updateFormDataBeforeSave);
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) {
582 + $(document).on('xwiki:actions:view', function() {
672 672   // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit
673 673   // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing
674 674   // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action
675 675   // buttons to have them ready for the next editing session (the user can save or cancel and then edit again
676 676   // without reloading the page).
677 - actionButtons.find(':input').blur().end().prop('disabled', true).parent().hide();
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 - }
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');
691 691   });
692 - return Promise.resolve($.get(XWiki.currentDocument.getURL('get'), {
592 + return $.get(XWiki.currentDocument.getURL('get'), {
693 693   xpage: 'editactions'
694 - })).then(html => {
594 + }).then(function(html) {
695 695   actionButtons.html(html);
696 696   // Fix the name of the Save & View action.
697 697   actionButtons.find('.btn-primary').first().attr('name', 'action_save');
698 - // Let the others know that the DOM has been updated, in order to enhance it.
699 - $(document).trigger('xwiki:dom:updated', {'elements': actionButtons.toArray()});
700 - return new Promise((resolve, reject) => {
701 - require(['xwiki-actionButtons', 'xwiki-diff', 'xwiki-autoSave'], function() {
702 - overrideAjaxSaveAndContinue();
703 - var xwikiDocument = actionButtons.data('xwikiDocument');
704 - // Enable the action buttons (and their shortcut keys) only if we're editing a document.
705 - actionButtons.prop('disabled', !xwikiDocument);
706 - resolve(xwikiDocument);
707 - });
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);
708 708   });
709 - }).catch(() => {
710 - new XWiki.widgets.Notification(l10n['edit.inplace.actionButtons.loadFailed'], 'error');
615 + return deferred.promise();
616 + }, function() {
617 + new XWiki.widgets.Notification(
618 + $jsontool.serialize($services.localization.render('edit.inplace.actionButtons.loadFailed')),
619 + 'error'
620 + );
711 711   });
712 712   };
713 713  
714 - var updateFormDataBeforeSave = function() {
715 - const form = $('form#inplace-editing');
716 - const xwikiDocument = form.children('.sticky-buttons').data('xwikiDocument');
717 -
718 - form.find('input[name="language"]').val(xwikiDocument.getRealLocale());
719 - form.find('input[name="isNew"]').val(xwikiDocument.isNew);
720 -
721 - // Submit either the raw (source) content (no syntax conversion needed in this case) or the rendered content (HTML)
722 - // in which case we have to force the conversion to the document syntax on the server.
723 - const submitRawContent = xwikiDocument.content !== xwikiDocument.originalDocument.content;
724 - form.find('input[name="content"]').val(submitRawContent ? xwikiDocument.content : xwikiDocument.renderedContent);
725 - form.find('input[name="RequiresHTMLConversion"]').prop('disabled', submitRawContent);
726 - form.find('input[name="content_syntax"]').val(xwikiDocument.syntax).prop('disabled', submitRawContent);
727 -
728 - // Add the temporary uploaded files to the form.
729 - $('#xwikicontent').nextAll('input[name="uploadedFiles"]').attr('form', 'inplace-editing');
730 -
731 - // Check for merge conflicts only if the document is not new and we know the current version.
732 - if (!xwikiDocument.isNew && xwikiDocument.version) {
733 - form.find('input[name="previousVersion"]').val(xwikiDocument.version);
734 - form.find('input[name="editingVersionDate"]').val(new Date(xwikiDocument.modified).getTime());
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);
735 735   }
736 736   };
737 737  
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 +
738 738   var overrideAjaxSaveAndContinue = function() {
739 739   var originalAjaxSaveAndContinue = $.extend({}, XWiki.actionButtons.AjaxSaveAndContinue.prototype);
740 740   $.extend(XWiki.actionButtons.AjaxSaveAndContinue.prototype, {
741 741   reloadEditor: function() {
742 - var actionButtons = $('.inplace-editing-buttons');
743 - if (actionButtons.is(':visible')) {
744 - // This function is called after the document save confirmation is received, if the save was done by merge. We
745 - // register our reload listener from a document saved listener, but we're using promises which are
746 - // asynchronous so the reload listener is actually registered with a delay. For this reason we trigger the
747 - // reload event with a delay to ensure our reload listener is called.
748 - setTimeout(function() {
749 - actionButtons.trigger('xwiki:actions:reload');
750 - }, 0);
701 + if ($('.inplace-editing-buttons').is(':visible')) {
702 + $(document).trigger('xwiki:actions:reload');
751 751   } else {
752 752   return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments);
753 753   }
... ... @@ -768,26 +768,18 @@
768 768  
769 769   var initTitleEditor = function(xwikiDocument) {
770 770   var label = $('<label for="document-title-input" class="sr-only"/>')
771 - .text(l10n['core.editors.content.titleField.label']);
772 - var input = $('<input type="text" id="document-title-input" name="title" form="inplace-editing" />')
773 - .val(xwikiDocument.rawTitle);
774 - if (config.titleIsMandatory) {
775 - input.attr({
776 - 'required': '',
777 - 'data-validation-value-missing': l10n['core.validation.required.message']
778 - });
779 - } else {
780 - var placeholder = xwikiDocument.documentReference.name;
781 - if (placeholder === 'WebHome') {
782 - placeholder = xwikiDocument.documentReference.parent.name;
783 - }
784 - input.attr('placeholder', placeholder);
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;
785 785   }
729 + input.attr('placeholder', placeholder);
786 786   $('#document-title h1').addClass('editable').empty().append([label, input]);
787 - $(document).on('xwiki:actions:beforeSave.titleEditor', '.xcontent.form', function(event) {
731 + $(document).on('xwiki:actions:beforeSave.titleEditor', function(event) {
788 788   xwikiDocument.rawTitle = input.val();
789 789   });
790 - $(document).one('xwiki:actions:view', '.xcontent.form', function(event, data) {
734 + $(document).one('xwiki:actions:view', function(event, data) {
791 791   // Destroy the title editor.
792 792   $(document).off('xwiki:actions:beforeSave.titleEditor');
793 793   $('#document-title h1').removeClass('editable').text(xwikiDocument.rawTitle);
... ... @@ -807,25 +807,28 @@
807 807   // Keep the focus while the edit content is being prepared.
808 808   viewContent.focus();
809 809   }
810 - var data = $.extend({}, config, {
754 + var data = {
755 + contentType: 'org.xwiki.rendering.syntax.SyntaxContent',
756 + editMode: 'wysiwyg',
811 811   document: xwikiDocument,
812 812   // The content editor is loaded on demand, asynchronously.
813 - deferred: $.Deferred()
814 - });
815 - editContent.trigger('xwiki:actions:edit', data);
816 - return data.deferred.promise().then(() => {
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() {
817 817   editContent.show();
818 818   viewContent.remove();
819 819   if (withFocus) {
820 - // Restore the focus when the edit content is ready but make sure we don't scroll the page. We don't restore the
821 - // focus right away because we just made the content visible so it may not be editable yet (e.g. the WYSIWYG
822 - // editor can make the content editable only if it is visible).
823 - setTimeout(function() {
824 - editContent[0].focus({preventScroll: true});
825 - }, 0);
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});
826 826   }
827 - return xwikiDocument;
828 828   });
774 + editContent.trigger('xwiki:actions:edit', data);
775 + return editContentPromise;
829 829   };
830 830  
831 831   var startRealTimeEditingSession = function(xwikiDocument) {
... ... @@ -844,10 +844,9 @@
844 844   };
845 845  
846 846   return {
847 - preload,
848 - editPage,
849 - editSection,
850 - translatePage
794 + preload: preload,
795 + editPage: editPage,
796 + editSection: editSection
851 851   };
852 852  });
853 853  
... ... @@ -857,7 +857,8 @@
857 857   return;
858 858   }
859 859  
860 - var wysiwygEditorModule = 'xwiki-' + config.wysiwygEditor + '-inline';
806 + var inplaceEditingConfig = $('div[data-inplace-editing-config]').data('inplaceEditingConfig') || {};
807 + var wysiwygEditorModule = 'xwiki-' + inplaceEditingConfig.wysiwygEditor + '-inline';
861 861  
862 862   var preloadEditor = function() {
863 863   require(['editInPlace', wysiwygEditorModule], function(editInPlace) {
... ... @@ -875,193 +875,44 @@
875 875   });
876 876   }
877 877  
878 - var onInPlaceEditing = function(event) {
825 + var editButton = $('#tmEdit > a');
826 + editButton.on('click.inPlaceEditing', function(event) {
827 + event.preventDefault();
879 879   // Make sure the user doesn't try to re-activate the edit mode while we are in edit mode.
880 - if (editButton.hasClass('disabled')) {
881 - return;
882 - }
883 - // Disable the edit buttons and hide the section edit links.
884 - editButton.add(translateButton).addClass('disabled');
885 - editButton.attr('aria-disabled', 'true');
886 - var reference = editButton.attr('href');
887 - editButton.removeAttr('href');
888 - editButton.attr('role', 'link');
889 - $('#xwikicontent').children(':header').children('.edit_section').addClass('hidden');
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) {
890 890   event.preventDefault();
891 - const handler = event.data;
892 - const data = handler.beforeEdit?.(event);
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');
893 893   // Load the code needed to edit in place only when the edit button is clicked.
894 894   require(['editInPlace', wysiwygEditorModule], function(editInPlace) {
895 - // Re-enable the translate button because it can be used while editing to create the missing translation.
896 - translateButton.removeClass('disabled');
897 - handler.edit(editInPlace, data).finally(function() {
898 - // Restore only the edit button at the end because:
899 - // * the translate button is restored (if needed) by the editInPlace module
900 - // * the section edit links are restored when the document is rendered for view
851 + editInPlace.editSection(heading.attr('id')).always(function() {
901 901   editButton.removeClass('disabled');
902 - editButton.removeAttr('aria-disabled');
903 - editButton.removeAttr('role');
904 - editButton.attr('href', reference);
905 905   });
906 906   // Fallback on the standalone edit mode if we fail to load the required modules.
907 - }, disableInPlaceEditing.bind(event.target));
908 - };
855 + }, $.proxy(disableInPlaceEditing, event.target));
856 + });
909 909  
910 910   var disableInPlaceEditing = function() {
911 - editButton.add(translateButton).off('click.inPlaceEditing').removeClass('disabled');
859 + editButton.off('click.inPlaceEditing').removeClass('disabled');
912 912   $('#xwikicontent').off('click.inPlaceEditing').removeAttr('tabindex').children(':header').children('.edit_section')
913 913   .removeClass('hidden');
914 914   // Fallback on the standalone edit mode.
915 915   $(this).click();
916 916   };
917 -
918 - var editButton = $(config.editButtonSelector);
919 - editButton.on('click.inPlaceEditing', {
920 - beforeEdit: function() {
921 - history.replaceState(null, null, '#edit');
922 - },
923 - edit: function(editInPlace) {
924 - return editInPlace.editPage();
925 - }
926 - }, onInPlaceEditing).attr('data-editor', 'inplace');
927 -
928 - var translateButton = $(config.translateButtonSelector);
929 - translateButton.on('click.inPlaceEditing', {
930 - beforeEdit: function() {
931 - history.replaceState(null, null, '#translate');
932 - translateButton.parent().addClass('hidden');
933 - },
934 - edit: function(editInPlace) {
935 - return editInPlace.translatePage();
936 - }
937 - }, onInPlaceEditing);
938 -
939 - // Section in-place editing.
940 - $('#xwikicontent').on('click.inPlaceEditing', '> :header > a.edit_section:not(.disabled)', {
941 - beforeEdit: function(event) {
942 - // Focus the content right away to give the user instant visual feedback about what is going to happen.
943 - $('#xwikicontent').attr('tabindex', '0').focus();
944 - // Return the id of the edited section.
945 - return $(event.target).closest(':header').attr('id');
946 - },
947 - edit: function(editInPlace, sectionId) {
948 - return editInPlace.editSection(sectionId);
949 - }
950 - }, onInPlaceEditing);
951 -
952 - if (window.location.hash === '#edit') {
953 - editButton.click();
954 - } else if (window.location.hash === '#translate') {
955 - translateButton.click();
956 - }
957 957  });
958 -
959 -require(['jquery'], function($) {
960 - // Backup the document title before each editing session in order to catch changes.
961 - var previousPlainTitle;
962 - $('#xwikicontent').on('xwiki:actions:edit', function(event, data) {
963 - previousPlainTitle = data.document.getPlainTitle();
964 - });
965 -
966 - // Update the UI after each editing session.
967 - $(document).on('xwiki:actions:view', function(event, data) {
968 - var xwikiDocument = data.document;
969 - updateDocAuthorAndDate(xwikiDocument);
970 - updateDocExtraTabs(xwikiDocument);
971 - updateDrawer(xwikiDocument);
972 - updateContentMenu(xwikiDocument);
973 - if (xwikiDocument.getPlainTitle() !== previousPlainTitle) {
974 - updateDocTrees(xwikiDocument);
975 - updateLinks(xwikiDocument);
976 - }
977 - });
978 -
979 - var updateDocAuthorAndDate = function(xwikiDocument) {
980 - var urlWithSelector = xwikiDocument.getURL('get', 'xpage=contentheader') + ' .xdocLastModification';
981 - $('.xdocLastModification').load(urlWithSelector, function() {
982 - // load() replaces the content of the specified container but we want to replace the container itself. We can't do
983 - // this from the selector, e.g. by using '.xdocLastModification > *' because we lose the text nodes.
984 - $(this).children().unwrap();
985 - });
986 - };
987 -
988 - var updateDocExtraTabs = function(xwikiDocument) {
989 - // Reload the selected tab and force the reload of the hidden tabs next time they are selected.
990 - $('#docextrapanes').children().addClass('empty').empty();
991 - var selectedTab = $('#docExtraTabs .active[data-template]');
992 - if (selectedTab.length) {
993 - var docExtraId = selectedTab.attr('id');
994 - docExtraId = docExtraId.substring(0, docExtraId.length - 'tab'.length);
995 - XWiki.displayDocExtra(docExtraId, selectedTab.data('template'), false);
996 - }
997 - };
998 -
999 - // Update the document trees (e.g. breadcrumb, navigation) if they have nodes that correspond to the edited document.
1000 - // Note that we want to update the internal tree data not just the link label. This is especially useful if we're
1001 - // going to implement refactoring operations (rename) in the document tree.
1002 - var updateDocTrees = function(xwikiDocument) {
1003 - var plainTitle = xwikiDocument.getPlainTitle();
1004 - $('.jstree-xwiki').each(function() {
1005 - $(this).jstree?.(true)?.set_text?.('document:' + xwikiDocument.id, plainTitle);
1006 - });
1007 - };
1008 -
1009 - // Update the links that target the edited document and whose label matches the document title. Note that this can
1010 - // update links whose label was not generated dynamically (e.g. with server side scripting) based on the document
1011 - // title. For instance there could be links with hard-coded labels or with labels generated using a translatin key
1012 - // (like in the Applications panel). For simplicity we assume that if the link matches the document URL and its
1013 - // previous title then it needs to be updated, but this happens only at the UI level.
1014 - var updateLinks = function(xwikiDocument) {
1015 - var docURL = xwikiDocument.getURL();
1016 - var newPlainTitle = xwikiDocument.getPlainTitle();
1017 - // Exclude the links from the document content.
1018 - // Update the links that contain only text (no child elements) otherwise we can lose UI elements (e.g. icons).
1019 - $('a').not('#xwikicontent a').not(':has(*)').filter(function() {
1020 - var linkURL = $(this).attr('href')?.split(/[?#]/, 1)[0];
1021 - return linkURL === docURL && $(this).text() === previousPlainTitle;
1022 - }).text(newPlainTitle);
1023 - };
1024 -
1025 - // Update the list of available document translations in the drawer menu. This is needed for instance when a new
1026 - // translation is created using the in-place editor.
1027 - var updateDrawer = function(xwikiDocument) {
1028 - var languageMenu = $('#tmLanguages_menu');
1029 - var locale = xwikiDocument.getRealLocale();
1030 - // Look for the language query string parameter, either inside or at the end.
1031 - var localeSelector = 'a[href*="language=' + locale + '&"], a[href$="language=' + locale + '"]';
1032 - // Check if the language menu is present (multilingual is on) and the document locale is not listed.
1033 - if (languageMenu.length && !languageMenu.find(localeSelector).length) {
1034 - // If we get here then it means a new document translation was created and it needs to be listed in the drawer.
1035 - $('<div/>').load(xwikiDocument.getURL('get', $.param({
1036 - 'xpage': 'xpart',
1037 - 'vm': 'drawer.vm',
1038 - 'useLayoutVars': true
1039 - // Pass the query string from the current URL so that it gets included in the translation URL.
1040 - // XWIKI-11314: Changing the current language from the UI does not preserve the query string of the current URL
1041 - })) + '&' + location.search.substring(1) + ' #tmLanguages_menu', function() {
1042 - $(this).find('a').each(function() {
1043 - // Clean the query string.
1044 - $(this).attr('href', $(this).attr('href').replace(/&?(xpage=xpart|vm=drawer\.vm|useLayoutVars=true)/g, '')
1045 - .replace('?&', '?'));
1046 - });
1047 - languageMenu.replaceWith($(this).children());
1048 - });
1049 - }
1050 - };
1051 -
1052 - // Update the links from the content menu to point to the real document locale. This is needed especially when a new
1053 - // document translation is created in-place.
1054 - var updateContentMenu = function(xwikiDocument) {
1055 - var realLocale = xwikiDocument.getRealLocale();
1056 - var defaultLocale = xwikiDocument.getDefaultLocale();
1057 - if (realLocale != defaultLocale) {
1058 - var defaultLocaleRegex = new RegExp('(\\blanguage=)' + defaultLocale + '($|&|#)');
1059 - $('#contentmenu a[href*="language=' + defaultLocale + '"]').each(function() {
1060 - $(this).attr('href', $(this).attr('href').replace(defaultLocaleRegex, '$1' + realLocale + '$2'));
1061 - });
1062 - }
1063 - };
1064 -});
1065 -
1066 -})(JSON.parse(document.querySelector('[data-inplace-editing-config]')?.getAttribute('data-inplace-editing-config')) ||
1067 - {});
Parse content
... ... @@ -1,1 +1,1 @@
1 -No
1 +Yes
XWiki.StyleSheetExtension[0]
Code
... ... @@ -9,9 +9,10 @@
9 9  @document-title-input-padding-vertical: @line-height-computed / 4 - 1;
10 10  input#document-title-input {
11 11   /* Preserve the heading styles. */
12 + border: 1px solid transparent;
13 + box-shadow: none;
12 12   color: inherit;
13 13   font-size: inherit;
14 - background-color: @body-bg;
15 15   /* It seems it's not enough to set the line height for the text input. We also need to set its height. */
16 16   height: @font-size-document-title * @headings-line-height + 2 * (1 + @document-title-input-padding-vertical);
17 17   line-height: @headings-line-height;
... ... @@ -18,16 +18,12 @@
18 18   padding: @document-title-input-padding-vertical (ceil(@grid-gutter-width / 2) - 1);
19 19   width: 100%;
20 20  }
21 -input#document-title-input:valid {
22 - border: 1px solid transparent;
23 - box-shadow: none;
24 -}
25 25  
26 -input#document-title-input:valid:hover {
23 +input#document-title-input:hover {
27 27   border-color: @input-border;
28 28  }
29 29  
30 -input#document-title-input:valid:focus,
27 +input#document-title-input:focus,
31 31  #xwikicontent[contenteditable]:focus,
32 32  #xwikicontent[tabindex]:focus {
33 33   .form-control-focus();
... ... @@ -53,7 +53,7 @@
53 53   padding-top: @line-height-computed * 0.75;
54 54  }
55 55  
56 -form#inplace-editing {
53 +.sticky-buttons-wrapper {
57 57   /* Leave some space for the bottom box shadow of the editing area. */
58 58   margin-top: 7px;
59 59  }
XWiki.UIExtensionClass[0]
Executed Content
... ... @@ -1,11 +9,3 @@
1 -{{velocity output="false"}}
2 -## TODO: Remove this when XWIKI-18511 (Add support for passing a query string when calling getSkinFile) is implemented.
3 -#macro (getSkinFileWithParams $file $params)
4 -#set ($url = $xwiki.getSkinFile($file, true))
5 -$url#if ($url.contains('?'))&#else?#end$escapetool.url($params)
6 -#end
7 -{{/velocity}}
8 -
9 9  {{velocity}}
10 10  {{html clean="false"}}
11 11  #if ($services.edit.document.inPlaceEditingEnabled() && $hasEdit && $xcontext.action == 'view' && !$doc.isNew())
... ... @@ -12,58 +12,10 @@
12 12   ## We support in-place editing only for the WYSIWYG edit mode ATM.
13 13   #getDefaultDocumentEditor($defaultEditMode)
14 14   #if ($defaultEditMode == 'wysiwyg')
15 - #set ($l10nKeys = [
16 - 'edit.inplace.page.renderFailed',
17 - 'edit.inplace.page.lockFailed',
18 - 'edit.inplace.close',
19 - 'edit.inplace.page.loadFailed',
20 - 'edit.inplace.actionButtons.loadFailed',
21 - 'core.editors.content.titleField.label',
22 - 'core.validation.required.message',
23 - ['edit.inplace.page.translate.messageBefore', $doc.realLocale.getDisplayName($xcontext.locale),
24 - $xcontext.locale.getDisplayName($xcontext.locale)],
25 - ['edit.inplace.page.translate.messageAfter', $xcontext.locale.getDisplayName($xcontext.locale)]
26 - ])
27 - #set ($l10n = {})
28 - #foreach ($key in $l10nKeys)
29 - #set ($params = $key.subList(1, $key.size()))
30 - #if ($params)
31 - #set ($discard = $l10n.put($key[0], $services.localization.render($key[0], $params)))
32 - #else
33 - #set ($discard = $l10n.put($key, $services.localization.render($key)))
34 - #end
35 - #end
36 - ## See stylesheets.vm
37 - #set ($cssParams = {
38 - 'skin': $xwiki.skin,
39 - 'colorTheme': $services.model.serialize($themeDoc.documentReference, 'default')
40 - })
41 - #set ($jsParams = {'language': $xcontext.locale})
42 - ## We have to explicitly enable the source mode for in-line edit because the latest version of the content editor
43 - ## could be installed on an older version of XWiki where the in-place editor didn't support the source mode (so the
44 - ## content editor cannot enable the source mode by default).
45 45   #set ($inplaceEditingConfig = {
46 - 'contentType': 'org.xwiki.rendering.syntax.SyntaxContent',
47 47   'editMode': $defaultEditMode,
48 - 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id,
49 - 'editButtonSelector': '#tmEdit > a',
50 - 'translateButtonSelector': '#tmTranslate > a',
51 - 'enableSourceMode': true,
52 - 'paths': {
53 - 'js': {
54 - 'xwiki-actionButtons': "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.js' $jsParams)",
55 - 'xwiki-autoSave': "#getSkinFileWithParams('js/xwiki/editors/autosave.js' $jsParams)",
56 - 'xwiki-diff': $xwiki.getSkinFile('uicomponents/viewers/diff.js')
57 - },
58 - 'css': [
59 - "#getSkinFileWithParams('js/xwiki/actionbuttons/actionButtons.css' $cssParams)",
60 - "#getSkinFileWithParams('js/xwiki/editors/autosave.css' $cssParams)",
61 - "#getSkinFileWithParams('uicomponents/viewers/diff.css' $cssParams)"
62 - ]
63 - },
64 - 'l10n': $l10n
9 + 'wysiwygEditor': $services.edit.syntaxContent.defaultWysiwygEditor.descriptor.id
65 65   })
66 - #set ($inplaceEditingConfig.titleIsMandatory = $xwiki.getSpacePreference('xwiki.title.mandatory') == 1)
67 67   <div class="hidden" data-inplace-editing-config="$escapetool.xml($jsontool.serialize($inplaceEditingConfig))"></div>
68 68   ## We didn't move this to the file system because it uses LESS and we didn't want to include it in the skin.
69 69   #set ($discard = $xwiki.ssx.use('XWiki.InplaceEditing'))
XWiki.UIExtensionClass[1]
Cached
... ... @@ -1,0 +1,1 @@
1 +No
Asynchronous rendering
... ... @@ -1,0 +1,1 @@
1 +No
Executed Content
... ... @@ -1,0 +1,22 @@
1 +{{velocity}}
2 +{{html clean="false"}}
3 +## Output the translation button if all the following conditions are met:
4 +## * multilingual is on
5 +## * we're loading the original document version
6 +## * the original document version has a locale specified (it doesn't make sense to translate technical documents)
7 +## * the current UI locale doesn't match the original document locale
8 +#if ($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,0 +1,1 @@
1 +org.xwiki.plaftorm.menu.content
Extension ID
... ... @@ -1,0 +1,1 @@
1 +org.xwiki.plaftorm.menu.content.translate
Extension Parameters
... ... @@ -1,0 +1,1 @@
1 +order=5000
Extension Scope
... ... @@ -1,0 +1,1 @@
1 +wiki