Modifiche per il documento InplaceEditing

Dalla versione 2.1
modificato da N Pompei
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
Cambia il commento: Install extension [org.xwiki.platform:xwiki-platform-edit-ui/15.10.5]

Summary

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(thisXWikiDocument, {
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 - // Render failed.
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 - thisXWikiDocument.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 - thisXWikiDocument.isNew = false;
111 - return $.extend(thisXWikiDocument, 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 - thisXWikiDocument.locked = action;
139 - return thisXWikiDocument;
140 - }, function(response) {
152 + this.locked = action;
153 + return this;
154 + }).catch(response => {
141 141   // Lock failed.
142 - delete thisXWikiDocument.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 - thisXWikiDocument.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().then(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.state() === '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 - 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();
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 - 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();
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 -
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