... |
... |
@@ -1,8 +1,9 @@ |
1 |
1 |
require.config({ |
2 |
2 |
paths: { |
3 |
3 |
'actionButtons': $jsontool.serialize($xwiki.getSkinFile('js/xwiki/actionbuttons/actionButtons.js', true)), |
|
4 |
+ 'autoSave': $jsontool.serialize($xwiki.getSkinFile('js/xwiki/editors/autosave.js', true)), |
4 |
4 |
// Required in case the user needs to resolve merge conflicts on save. |
5 |
|
- 'diff': $jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.js')) |
|
6 |
+ 'xwiki-diff': $jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.js')) |
6 |
6 |
} |
7 |
7 |
}); |
8 |
8 |
|
... |
... |
@@ -17,7 +17,17 @@ |
17 |
17 |
* @return this document's real locale |
18 |
18 |
*/ |
19 |
19 |
getRealLocale: function() { |
20 |
|
- return this.language || (this.translations && this.translations['default']) || $('html').attr('lang'); |
|
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 |
+ } |
|
31 |
+ return realLocale; |
21 |
21 |
}, |
22 |
22 |
|
23 |
23 |
/** |
... |
... |
@@ -55,7 +55,7 @@ |
55 |
55 |
*/ |
56 |
56 |
render: function(forView) { |
57 |
57 |
var queryString = { |
58 |
|
- xpage: 'rendercontent', |
|
69 |
+ xpage: 'get', |
59 |
59 |
outputTitle: true, |
60 |
60 |
outputSyntax: forView ? null : 'annotatedxhtml', |
61 |
61 |
language: this.getRealLocale(), |
... |
... |
@@ -69,11 +69,15 @@ |
69 |
69 |
'error' |
70 |
70 |
); |
71 |
71 |
}).then(function(html) { |
|
83 |
+ // Render succeeded. |
72 |
72 |
var container = $('<div/>').html(html); |
73 |
73 |
return $.extend(thisXWikiDocument, { |
74 |
74 |
renderedTitle: container.find('#document-title h1').html(), |
75 |
75 |
renderedContent: container.find('#xwikicontent').html() |
76 |
76 |
}); |
|
89 |
+ }, function() { |
|
90 |
+ // Render failed. |
|
91 |
+ return thisXWikiDocument; |
77 |
77 |
}); |
78 |
78 |
}, |
79 |
79 |
|
... |
... |
@@ -88,11 +88,15 @@ |
88 |
88 |
// Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). |
89 |
89 |
timestamp: new Date().getTime() |
90 |
90 |
}).then(function(newXWikiDocument) { |
|
106 |
+ // Reload succeeded. |
91 |
91 |
// Resolve the document reference. |
92 |
92 |
thisXWikiDocument.documentReference = XWiki.Model.resolve(newXWikiDocument.id, XWiki.EntityType.DOCUMENT); |
93 |
93 |
// We were able to load the document so it's not new. |
94 |
94 |
thisXWikiDocument.isNew = false; |
95 |
95 |
return $.extend(thisXWikiDocument, newXWikiDocument); |
|
112 |
+ }, function() { |
|
113 |
+ // Reload failed. |
|
114 |
+ return thisXWikiDocument; |
96 |
96 |
}); |
97 |
97 |
}, |
98 |
98 |
|
... |
... |
@@ -115,8 +115,19 @@ |
115 |
115 |
// Make sure the response is not retrieved from cache (IE11 doesn't obey the caching HTTP headers). |
116 |
116 |
timestamp: new Date().getTime() |
117 |
117 |
}).then(function() { |
|
137 |
+ // Lock succeeded. |
118 |
118 |
thisXWikiDocument.locked = action; |
119 |
119 |
return thisXWikiDocument; |
|
140 |
+ }, function(response) { |
|
141 |
+ // Lock failed. |
|
142 |
+ delete thisXWikiDocument.locked; |
|
143 |
+ // Check if the user can force the lock. |
|
144 |
+ var lockConfirmation = response.responseJSON; |
|
145 |
+ if (response.status === 423 && lockConfirmation) { |
|
146 |
+ // The user can force the lock, but needs confirmation. |
|
147 |
+ thisXWikiDocument.lockConfirmation = lockConfirmation; |
|
148 |
+ } |
|
149 |
+ return thisXWikiDocument; |
120 |
120 |
}); |
121 |
121 |
}, |
122 |
122 |
|
... |
... |
@@ -156,6 +156,7 @@ |
156 |
156 |
], function($, xcontext, xwikiDocumentAPI) { |
157 |
157 |
var preload = function() { |
158 |
158 |
loadCSS($jsontool.serialize($xwiki.getSkinFile('js/xwiki/actionbuttons/actionButtons.css', true))); |
|
189 |
+ loadCSS($jsontool.serialize($xwiki.getSkinFile('js/xwiki/editors/autosave.css', true))); |
159 |
159 |
// Required in case the user needs to resolve merge conflicts on save. |
160 |
160 |
loadCSS($jsontool.serialize($xwiki.getSkinFile('uicomponents/viewers/diff.css', true))); |
161 |
161 |
return initActionButtons(); |
... |
... |
@@ -225,20 +225,27 @@ |
225 |
225 |
lockFailed: function() {} |
226 |
226 |
}, options); |
227 |
227 |
$('#xwikicontent').addClass('loading'); |
|
259 |
+ // Lock the document first. |
228 |
228 |
return lock(currentXWikiDocument).fail(options.lockFailed) |
|
261 |
+ // Then load the document only if we managed to lock it. |
229 |
229 |
.then(load) |
|
263 |
+ // Then load the editors only if we managed to load the document. |
230 |
230 |
.then(edit).done(options.afterEdit).always(function() { |
231 |
231 |
$('#xwikicontent').removeClass('loading'); |
|
266 |
+ // Then wait for an action (save, cancel, reload) only if the editors were loaded successfuly. |
232 |
232 |
}).then(maybeSave) |
233 |
|
- .then(unlock) |
234 |
|
- .then(view); |
|
268 |
+ // Then unlock the document both when the edit ended with success and with a failure. |
|
269 |
+ .then(unlock, unlock) |
|
270 |
+ // Finally view the document both when the edit ended with success and with a failure. |
|
271 |
+ .then(view, view); |
235 |
235 |
}; |
236 |
236 |
|
237 |
237 |
var lock = function(xwikiDocument) { |
238 |
|
- return xwikiDocument.lock().then(null, function(response) { |
239 |
|
- var confirmation = response.responseJSON; |
|
275 |
+ return xwikiDocument.lock().then(null, function(xwikiDocument) { |
240 |
240 |
// If the document was already locked then we need to ask the user if they want to force the lock. |
241 |
|
- if (response.status === 423 && confirmation) { |
|
277 |
+ if (xwikiDocument.lockConfirmation) { |
|
278 |
+ var confirmation = xwikiDocument.lockConfirmation; |
|
279 |
+ delete xwikiDocument.lockConfirmation; |
242 |
242 |
return maybeForceLock(confirmation).then($.proxy(xwikiDocument, 'lock', 'edit', true)); |
243 |
243 |
} else { |
244 |
244 |
new XWiki.widgets.Notification( |
... |
... |
@@ -245,6 +245,7 @@ |
245 |
245 |
$jsontool.serialize($services.localization.render('edit.inplace.page.lockFailed')), |
246 |
246 |
'error' |
247 |
247 |
); |
|
286 |
+ return xwikiDocument; |
248 |
248 |
} |
249 |
249 |
}); |
250 |
250 |
}; |
... |
... |
@@ -368,24 +368,18 @@ |
368 |
368 |
}; |
369 |
369 |
|
370 |
370 |
var save = function(data) { |
371 |
|
- // Push the changes to the server then render the document for view. We need the view HTML both if we stop editing |
372 |
|
- // now and if we continue but cancel the edit later. |
373 |
|
- return push(data.document).then($.proxy(render, null, true)).then(function(xwikiDocument) { |
|
410 |
+ // Push the changes to the server. |
|
411 |
+ return push(data.document).then(function(xwikiDocument) { |
374 |
374 |
// Save succeeded. |
375 |
|
- if (data['continue']) { |
376 |
|
- // Update the original version in order to be able to restore it on cancel. |
377 |
|
- delete xwikiDocument.originalDocument; |
378 |
|
- xwikiDocument.originalDocument = $.extend(true, {}, xwikiDocument); |
379 |
|
- // Continue editing. |
380 |
|
- return maybeSave(xwikiDocument); |
381 |
|
- } else { |
382 |
|
- // This is the final version. |
383 |
|
- return xwikiDocument; |
384 |
|
- } |
385 |
|
- }, function(xwikiDocument) { |
386 |
|
- // Save failed. Continue editing. |
387 |
|
- return maybeSave(xwikiDocument); |
388 |
|
- }); |
|
413 |
+ return shouldReload(xwikiDocument).then( |
|
414 |
+ // The document was saved with merge and thus if we want to continue eding we need to reload the editor (because |
|
415 |
+ // its content doesn't match the saved content). |
|
416 |
+ reload, |
|
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']) |
|
419 |
+ ); |
|
420 |
+ // Save failed. Continue editing because we may have unsaved content. |
|
421 |
+ }, maybeSave); |
389 |
389 |
}; |
390 |
390 |
|
391 |
391 |
var push = function(xwikiDocument) { |
... |
... |
@@ -398,9 +398,33 @@ |
398 |
398 |
// effect. |
399 |
399 |
$(document).one('xwiki:document:saved', $.proxy(deferred, 'resolve', xwikiDocument)); |
400 |
400 |
$(document).one('xwiki:document:saveFailed', $.proxy(deferred, 'reject', xwikiDocument)); |
401 |
|
- return deferred.promise().then($.proxy(xwikiDocument, 'reload')); |
|
434 |
+ return deferred.promise(); |
402 |
402 |
}; |
403 |
403 |
|
|
437 |
+ var maybeContinueEditing = function(continueEditing, xwikiDocument) { |
|
438 |
+ var afterReloadAndRender = function(success, xwikiDocument) { |
|
439 |
+ if (continueEditing) { |
|
440 |
+ if (success) { |
|
441 |
+ // Update the original version in order to be able to restore it on cancel. |
|
442 |
+ delete xwikiDocument.originalDocument; |
|
443 |
+ xwikiDocument.originalDocument = $.extend(true, {}, xwikiDocument); |
|
444 |
+ } |
|
445 |
+ // Continue editing. |
|
446 |
+ return maybeSave(xwikiDocument); |
|
447 |
+ } else { |
|
448 |
+ // This is the final version. We stop editing even if the reload / render failed. |
|
449 |
+ return xwikiDocument; |
|
450 |
+ } |
|
451 |
+ }; |
|
452 |
+ |
|
453 |
+ // Reload the document JSON data (to have the new version) and render the document for view. We need the view HTML |
|
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) |
|
458 |
+ ); |
|
459 |
+ }; |
|
460 |
+ |
404 |
404 |
var cancel = function(xwikiDocument) { |
405 |
405 |
// Simply return the original version to be restored. |
406 |
406 |
return xwikiDocument.originalDocument; |
... |
... |
@@ -418,6 +418,25 @@ |
418 |
418 |
// Make sure we unlock the document when the user navigates to another page. |
419 |
419 |
$(window).on('unload pagehide', $.proxy(unlock, null, currentXWikiDocument)); |
420 |
420 |
|
|
478 |
+ var shouldReload = function(xwikiDocument) { |
|
479 |
+ var reloadEventFired = false; |
|
480 |
+ $(document).one('xwiki:actions:reload.maybe', function() { |
|
481 |
+ reloadEventFired = true; |
|
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(); |
|
495 |
+ }; |
|
496 |
+ |
421 |
421 |
var reload = function(xwikiDocument) { |
422 |
422 |
// Leave the edit mode and then re-enter. |
423 |
423 |
return view(xwikiDocument, true).then(editInPlace); |
... |
... |
@@ -504,8 +504,12 @@ |
504 |
504 |
|
505 |
505 |
var loadActionButtons = function(actionButtons) { |
506 |
506 |
$(document).on('xwiki:actions:view', function() { |
507 |
|
- // Hide the action buttons and disable the shortcut keys (by disabling the buttons). |
508 |
|
- actionButtons.hide().find(':input').prop('disabled', true); |
|
583 |
+ // Blur the action buttons first to re-enable the "disabled in inputs" shortcut keys (e.g. the page edit |
|
584 |
+ // shortcut), then disable the action buttons in order to disable their shortcut keys while we're not editing |
|
585 |
+ // in-place (e.g. prevent the Save shortcut while the user is only viewing the page). Finally hide the action |
|
586 |
+ // buttons to have them ready for the next editing session (the user can save or cancel and then edit again |
|
587 |
+ // without reloading the page). |
|
588 |
+ actionButtons.find(':input').blur().prop('disabled', true).end().hide(); |
509 |
509 |
// Hide the translate button because it can be used only in edit mode for the moment. |
510 |
510 |
$('#tmTranslate').addClass('hidden'); |
511 |
511 |
}); |
... |
... |
@@ -520,9 +520,13 @@ |
520 |
520 |
// We need a place where actionButtons.js can add more hidden inputs. |
521 |
521 |
actionButtons.append('<div class="hidden extra"/>'); |
522 |
522 |
var deferred = $.Deferred(); |
523 |
|
- require(['actionButtons'], function() { |
|
603 |
+ require(['actionButtons', 'xwiki-diff', 'autoSave'], function() { |
524 |
524 |
overrideEditActions(); |
525 |
525 |
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}); |
526 |
526 |
var xwikiDocument = actionButtons.data('xwikiDocument'); |
527 |
527 |
// Enable the action buttons (and their shortcut keys) only if we're editing a document. |
528 |
528 |
actionButtons.find(':input').prop('disabled', !xwikiDocument); |
... |
... |
@@ -559,6 +559,9 @@ |
559 |
559 |
insert: function(element) { |
560 |
560 |
this._getActionButtons().find('.hidden.extra').append(element); |
561 |
561 |
}, |
|
646 |
+ down: function(selector) { |
|
647 |
+ return this._getActionButtons().find(selector)[0]; |
|
648 |
+ }, |
562 |
562 |
serialize: function() { |
563 |
563 |
var extra = this._getActionButtons().find(':input').serializeArray().reduce(function(extra, entry) { |
564 |
564 |
var value = extra[entry.name] || []; |
... |
... |
@@ -568,13 +568,21 @@ |
568 |
568 |
}, {}); |
569 |
569 |
var xwikiDocument = this._getActionButtons().data('xwikiDocument'); |
570 |
570 |
var formData = { |
571 |
|
- title: xwikiDocument.title, |
572 |
|
- content: xwikiDocument.renderedContent, |
573 |
|
- RequiresHTMLConversion: 'content', |
574 |
|
- content_syntax: xwikiDocument.syntax, |
|
658 |
+ title: xwikiDocument.rawTitle, |
575 |
575 |
language: xwikiDocument.getRealLocale(), |
576 |
576 |
isNew: xwikiDocument.isNew |
577 |
577 |
}; |
|
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 |
+ } |
578 |
578 |
// Check for merge conflicts only if the document is not new and we know the current version. |
579 |
579 |
if (!xwikiDocument.isNew && xwikiDocument.version) { |
580 |
580 |
formData.previousVersion = xwikiDocument.version; |
... |
... |
@@ -609,11 +609,14 @@ |
609 |
609 |
return originalAjaxSaveAndContinue.reloadEditor.apply(this, arguments); |
610 |
610 |
} |
611 |
611 |
}, |
612 |
|
- maybeRedirect: function() { |
|
707 |
+ maybeRedirect: function(continueEditing) { |
613 |
613 |
if ($('.inplace-editing-buttons').is(':visible')) { |
614 |
|
- // Never redirect when leaving the edit mode because we're already in view mode. |
615 |
|
- return false; |
|
709 |
+ // Overwrite the default behavior so that we don't redirect when leaving the edit mode because we're already |
|
710 |
+ // in view mode. We still need to report a redirect (return true) if we don't continue editing, so that |
|
711 |
+ // actionButtons.js behaves as if a redirect was done. |
|
712 |
+ return !continueEditing; |
616 |
616 |
} else { |
|
714 |
+ // Fallback on the default behavior if the in-place editing buttons are hidden. |
617 |
617 |
return originalAjaxSaveAndContinue.maybeRedirect.apply(this, arguments); |
618 |
618 |
} |
619 |
619 |
} |
... |
... |
@@ -623,7 +623,7 @@ |
623 |
623 |
var initTitleEditor = function(xwikiDocument) { |
624 |
624 |
var label = $('<label for="document-title-input" class="sr-only"/>') |
625 |
625 |
.text($jsontool.serialize($services.localization.render('core.editors.content.titleField.label'))); |
626 |
|
- var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.title); |
|
724 |
+ var input = $('<input type="text" id="document-title-input"/>').val(xwikiDocument.rawTitle); |
627 |
627 |
var placeholder = xwikiDocument.documentReference.name; |
628 |
628 |
if (placeholder === 'WebHome') { |
629 |
629 |
placeholder = xwikiDocument.documentReference.parent.name; |
... |
... |
@@ -631,12 +631,12 @@ |
631 |
631 |
input.attr('placeholder', placeholder); |
632 |
632 |
$('#document-title h1').addClass('editable').empty().append([label, input]); |
633 |
633 |
$(document).on('xwiki:actions:beforeSave.titleEditor', function(event) { |
634 |
|
- xwikiDocument.title = input.val(); |
|
732 |
+ xwikiDocument.rawTitle = input.val(); |
635 |
635 |
}); |
636 |
636 |
$(document).one('xwiki:actions:view', function(event, data) { |
637 |
637 |
// Destroy the title editor. |
638 |
638 |
$(document).off('xwiki:actions:beforeSave.titleEditor'); |
639 |
|
- $('#document-title h1').removeClass('editable').text(xwikiDocument.title); |
|
737 |
+ $('#document-title h1').removeClass('editable').text(xwikiDocument.rawTitle); |
640 |
640 |
}); |
641 |
641 |
return xwikiDocument; |
642 |
642 |
}; |
... |
... |
@@ -658,7 +658,11 @@ |
658 |
658 |
editMode: 'wysiwyg', |
659 |
659 |
document: xwikiDocument, |
660 |
660 |
// The content editor is loaded on demand, asynchronously. |
661 |
|
- deferred: $.Deferred() |
|
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 |
662 |
662 |
}; |
663 |
663 |
var editContentPromise = data.deferred.promise(); |
664 |
664 |
editContentPromise.done(function() { |
... |
... |
@@ -696,6 +696,11 @@ |
696 |
696 |
}); |
697 |
697 |
|
698 |
698 |
require(['jquery'], function($) { |
|
801 |
+ // We can edit in-place only if the #xwikicontent element is present. |
|
802 |
+ if (!$('#xwikicontent').length) { |
|
803 |
+ return; |
|
804 |
+ } |
|
805 |
+ |
699 |
699 |
var inplaceEditingConfig = $('div[data-inplace-editing-config]').data('inplaceEditingConfig') || {}; |
700 |
700 |
var wysiwygEditorModule = 'xwiki-' + inplaceEditingConfig.wysiwygEditor + '-inline'; |
701 |
701 |
|