-
Notifications
You must be signed in to change notification settings - Fork 96
/
LeftAndMain.EditForm.js
548 lines (497 loc) · 17.7 KB
/
LeftAndMain.EditForm.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
/**
* File: LeftAndMain.EditForm.js
*/
import $ from 'jquery';
import i18n from 'i18n';
const currBeforeUnload = window.onbeforeunload;
// Can't bind this through jQuery
window.onbeforeunload = function(e) {
var form = $('.cms-edit-form');
form.trigger('beforesubmitform');
if(form.is('.changed') && ! form.is('.discardchanges')) {
return i18n._t('Admin.CONFIRMUNSAVEDSHORT');
}
if (typeof currBeforeUnload === 'function') {
return currBeforeUnload();
}
return undefined;
};
$.entwine('ss', function($){
/**
* Class: .cms-edit-form
*
* Base edit form, provides ajaxified saving
* and reloading itself through the ajax return values.
* Takes care of resizing tabsets within the layout container.
*
* Change tracking is enabled on all fields within the form. If you want
* to disable change tracking for a specific field, add a "no-change-track"
* class to it.
*
* @name ss.Form_EditForm
* @require jquery.changetracker
*
* Events:
* ajaxsubmit - Form is about to be submitted through ajax
* validate - Contains validation result
* load - Form is about to be loaded through ajax
*/
$('.cms-edit-form').entwine(/** @lends ss.Form_EditForm */{
/**
* Variable: PlaceholderHtml
* (String_ HTML text to show when no form content is chosen.
* Will show inside the <form> tag.
*/
PlaceholderHtml: '',
/**
* Variable: ChangeTrackerOptions
* (Object)
*/
ChangeTrackerOptions: {
ignoreFieldSelector: '.no-change-track, .ss-upload :input, .cms-navigator :input'
},
/**
* Variable: ValidationErrorShown
* Boolean for tracking whether a validation error has been already been shown. Used because tabs can
* sometimes be inadvertently initialised multiple times, but we don't want duplicate messages
* (Boolean)
*/
ValidationErrorShown: false,
/**
* Constructor: onmatch
*/
onadd: function() {
var self = this;
// Turn off autocomplete to fix the access tab randomly switching radio buttons in Firefox
// when refresh the page with an anchor tag in the URL. E.g: /admin#Root_Access.
// Autocomplete in the CMS also causes strangeness in other browsers,
// filling out sections of the form that the user does not want to be filled out,
// so this turns it off for all browsers.
// See the following page for demo and explanation of the Firefox bug:
// http://www.ryancramer.com/journal/entries/radio_buttons_firefox/
this.attr("autocomplete", "off");
this._setupChangeTracker();
// Catch navigation events before they reach handleStateChange(),
// in order to avoid changing the menu state if the action is cancelled by the user
// $('.cms-menu')
// Optionally get the form attributes from embedded fields, see Form->formHtmlContent()
for(var overrideAttr in {'action':true,'method':true,'enctype':true,'name':true}) {
var el = this.find(':input[name='+ '_form_' + overrideAttr + ']');
if(el) {
this.attr(overrideAttr, el.val());
el.remove();
}
}
// Reset error display
this.setValidationErrorShown(false);
// TODO
// // Rewrite # links
// html = html.replace(/(<a[^>]+href *= *")#/g, '$1' + window.location.href.replace(/#.*$/,'') + '#');
//
// // Rewrite iframe links (for IE)
// html = html.replace(/(<iframe[^>]*src=")([^"]+)("[^>]*>)/g, '$1' + $('base').attr('href') + '$2$3');
this._super();
},
'from .cms-tabset': {
onafterredrawtabs: function () {
// Show validation errors if necessary
if(this.hasClass('validationerror')) {
// Ensure the first validation error is visible
var tabError = this.find('.message.validation, .message.required').first().closest('.tab');
$('.cms-container').clearCurrentTabState(); // clear state to avoid override later on
// Attempt #1: Look for nearest .ss-tabset (usually nested deeper underneath a .cms-tabset).
var $tabSet = tabError.closest('.ss-tabset');
// Attempt #2: Next level in tab-ception, try to select the tab within this higher level .cms-tabset if possible
if (!$tabSet.length) {
$tabSet = tabError.closest('.cms-tabset');
}
if ($tabSet.length) {
$tabSet.tabs('option', 'active', tabError.index('.tab'));
} else if (!this.getValidationErrorShown()) {
// Ensure that this error message popup won't be added more than once
this.setValidationErrorShown(true);
errorMessage(ss.i18n._t('Admin.VALIDATIONERROR', 'Validation Error'));
}
}
}
},
onremove: function() {
this.changetracker('destroy');
this._super();
},
onmatch: function() {
this._super();
},
onunmatch: function() {
this._super();
},
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Force initialization of tabsets to avoid layout glitches
this.add(this.find('.cms-tabset')).redrawTabs();
this.find('.cms-content-header').redraw();
},
/**
* Function: _setupChangeTracker
*/
_setupChangeTracker: function() {
// Don't bind any events here, as we dont replace the
// full <form> tag by any ajax updates they won't automatically reapply
this.changetracker(this.getChangeTrackerOptions());
},
/**
* Function: confirmUnsavedChanges
*
* Checks the jquery.changetracker plugin status for this form,
* and asks the user for confirmation via a browser dialog if changes are detected.
* Doesn't cancel any unload or form removal events, you'll need to implement this based on the return
* value of this message.
*
* If changes are confirmed for discard, the 'changed' flag is reset.
*
* Returns:
* (Boolean) FALSE if the user wants to abort with changes present, TRUE if no changes are detected
* or the user wants to discard them.
*/
confirmUnsavedChanges: function() {
this.trigger('beforesubmitform');
if(!this.is('.changed') || this.is('.discardchanges')) {
return true;
}
if (this.find('.btn-toolbar :submit.btn--loading.loading').length > 0) {
return true;
}
var confirmed = confirm(i18n._t('Admin.CONFIRMUNSAVED'));
if(confirmed) {
// Ensures that once a form is confirmed, subsequent
// changes to the underlying form don't trigger
// additional change confirmation requests
this.addClass('discardchanges');
}
return confirmed;
},
/**
* Function: onsubmit
*
* Suppress submission unless it is handled through ajaxSubmit().
*/
onsubmit: function(e, button) {
// Only submit if a button is present.
// This supressed submits from ENTER keys in input fields,
// which means the browser auto-selects the first available form button.
// This might be an unrelated button of the form field,
// or a destructive action (if "save" is not available, or not on first position).
if(this.prop("target") != "_blank") {
if(button) this.closest('.cms-container').submitForm(this, button);
return false;
}
},
/**
* Function: validate
*
* Hook in (optional) validation routines.
* Currently clientside validation is not supported out of the box in the CMS.
*
* Todo:
* Placeholder implementation
*
* Returns:
* {boolean}
*/
validate: function() {
var isValid = true;
this.trigger('validate', {isValid: isValid});
return isValid;
},
/*
* Track focus on htmleditor fields
*/
'from .htmleditor': {
oneditorinit: function(e){
var self = this,
field = $(e.target).closest('.field.htmleditor'),
editor = field.find('textarea.htmleditor').getEditor().getInstance();
// TinyMCE 4 will add a focus event, but for now, use click
editor.onClick.add(function(e){
self.saveFieldFocus(field.attr('id'));
});
}
},
/*
* Track focus on inputs
*/
'from .cms-edit-form :input:not(:submit)': {
onclick: function(e){
this.saveFieldFocus($(e.target).attr('id'));
},
onfocus: function(e){
this.saveFieldFocus($(e.target).attr('id'));
}
},
/*
* Track focus on treedropdownfields.
*/
'from .cms-edit-form .treedropdown *': {
onfocusin: function(e){
var field = $(e.target).closest('.field.treedropdown');
this.saveFieldFocus(field.attr('id'));
}
},
/*
* Track focus on chosen selects
*/
'from .cms-edit-form .dropdown .chosen-container a': {
onfocusin: function(e){
var field = $(e.target).closest('.field.dropdown');
this.saveFieldFocus(field.attr('id'));
}
},
/*
* Restore fields after tabs are restored
*/
'from .cms-container': {
ontabstaterestored: function(e){
this.restoreFieldFocus();
}
},
/*
* Saves focus in Window session storage so it that can be restored on page load
*/
saveFieldFocus: function(selected){
if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;
var id = $(this).attr('id'),
focusElements = [];
focusElements.push({
id:id,
selected:selected
});
if(focusElements) {
try {
window.sessionStorage.setItem(id, JSON.stringify(focusElements));
} catch(err) {
if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) {
// If this fails we ignore the error as the only issue is that it
// does not remember the focus state.
// This is a Safari bug which happens when private browsing is enabled.
return;
} else {
throw err;
}
}
}
},
/**
* Set focus or window to previously saved fields.
* Requires HTML5 sessionStorage support.
*
* Must follow tab restoration, as reliant on active tab
*/
restoreFieldFocus: function(){
if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;
var self = this,
hasSessionStorage = (typeof(window.sessionStorage)!=="undefined" && window.sessionStorage),
sessionData = hasSessionStorage ? window.sessionStorage.getItem(this.attr('id')) : null,
sessionStates = sessionData ? JSON.parse(sessionData) : false,
elementID,
tabbed = (this.find('.ss-tabset').length !== 0),
activeTab,
elementTab,
toggleComposite,
scrollY;
if(hasSessionStorage && sessionStates.length > 0){
$.each(sessionStates, function(i, sessionState) {
if(self.is('#' + sessionState.id)){
elementID = $('#' + sessionState.selected);
}
});
// If the element IDs saved in session states don't match up to anything in this particular form
// that probably means we haven't encountered this form yet, so focus on the first input
if($(elementID).length < 1){
this.focusFirstInput();
return;
}
activeTab = $(elementID).closest('.ss-tabset').find('.ui-tabs-nav .ui-tabs-active .ui-tabs-anchor').attr('id');
elementTab = 'tab-' + $(elementID).closest('.ss-tabset .ui-tabs-panel').attr('id');
// Last focussed element differs to last selected tab, do nothing
if(tabbed && elementTab !== activeTab){
return;
}
toggleComposite = $(elementID).closest('.togglecomposite');
//Reopen toggle fields
if(toggleComposite.length > 0){
toggleComposite.accordion('activate', toggleComposite.find('.ui-accordion-header'));
}
//Calculate position for scroll
scrollY = $(elementID).position().top;
//Fall back to nearest visible element if hidden (for select type fields)
if(!$(elementID).is(':visible')){
elementID = '#' + $(elementID).closest('.field').attr('id');
scrollY = $(elementID).position().top;
}
//set focus to focus variable if element focusable
$(elementID).focus();
// Scroll fallback when element is not focusable
// Only scroll if element at least half way down window
if(scrollY > $(window).height() / 2){
self.find('.cms-content-fields').scrollTop(scrollY);
}
} else {
// If session storage is not supported or there is nothing stored yet, focus on the first input
this.focusFirstInput();
}
},
/**
* Skip if an element in the form is already focused. Exclude elements which specifically
* opt-out of this behaviour via "data-skip-autofocus". This opt-out is useful if the
* first visible field is shown far down a scrollable area, for example for the pagination
* input field after a long GridField listing.
*/
focusFirstInput: function() {
this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:first').focus();
}
});
/**
* Class: .cms-edit-form .btn-toolbar :submit
*
* All buttons in the right CMS form go through here by default.
* We need this onclick overloading because we can't get to the
* clicked button from a form.onsubmit event.
*/
$('.cms-edit-form .btn-toolbar input.action[type=submit], .cms-edit-form .btn-toolbar button.action').entwine({
/**
* Function: onclick
*/
onclick: function(e) {
if(this.is(':disabled')) {
e.preventDefault();
return false;
}
// only run submit if other handlers didn't preventDefault
if (this._super(e) !== false && !e.defaultPrevented && !e.isDefaultPrevented()) {
this.parents('form').trigger('submit', [this]);
e.preventDefault();
return false;
}
}
});
/**
* If we've a history state to go back to, go back, otherwise fall back to
* submitting the form with the 'doCancel' action.
*/
$('.cms-edit-form .btn-toolbar input.action[type=submit].ss-ui-action-cancel, .cms-edit-form .btn-toolbar button.action.ss-ui-action-cancel').entwine({
onclick: function(e) {
if (window.history.length > 1) {
window.history.back();
} else {
this.parents('form').trigger('submit', [this]);
}
e.preventDefault();
}
});
// Hack: silverstripe/versioned coupling
$('.cms-edit-form .btn-toolbar button[name=action_doUnpublish].btn.action').entwine({
onclick: function(e) {
var owners = this.data('owners');
if (owners && parseInt(owners) > 0) {
var message = [
i18n.inject(
i18n._t(
'Admin.OWNED_WARNING_1',
'You are unpublishing content that is being used in {count} other published section(s).'
),
{ count: owners }
),
i18n._t(
'Admin.OWNED_WARNING_2',
'This could cause a published page to have missing components on the live site.'
),
i18n._t(
'Admin.OWNED_WARNING_3',
'Do you want to unpublish anyway?'
)
];
if (window.confirm(message.join('\n\n'))) {
this._super();
} else {
e.preventDefault();
}
} else {
this._super();
}
}
});
/**
* Hide tabs when only one is available.
* Special case is actiontabs - tabs between buttons, where we want to have
* extra options hidden within a tab (even if only one) by default.
*/
$('.cms-edit-form .ss-tabset').entwine({
onmatch: function() {
if (!this.hasClass('ss-ui-action-tabset')) {
var tabs = this.find("> ul:first");
if(tabs.children("li").length == 1) {
tabs.hide().parent().addClass("ss-tabset-tabshidden");
}
}
this._super();
},
onunmatch: function() {
this._super();
}
});
/**
* Class: .cms-edit-form #CanViewType, .cms-edit-form #CanEditType
*
* Toggle display of group dropdown in "access" tab,
* based on selection of radiobuttons.
*/
$('.cms-edit-form [name="CanViewType"], ' +
'.cms-edit-form [name="CanEditType"], ' +
'.cms-edit-form [name="CanCreateTopLevelType"]').entwine({
onmatch: function () {
if (this.val() === 'OnlyTheseUsers') {
if (this.is(':checked')) {
this.showList(true);
} else {
this.hideList(true);
}
}
},
onchange: function (e) {
if (e.target.value === 'OnlyTheseUsers') {
this.showList();
} else {
this.hideList();
}
},
showList: function (instant) {
const holder = this.closest('.field');
const list = holder.next().filter('.listbox, .treedropdown, .treemultiselect');
holder.addClass('field--merge-below');
if (instant) {
list.show().css('overflow', 'visible');
} else {
list.slideDown(() => {
// jquery adding overflow:hidden in hide, this will break listbox display
list.css('overflow', 'visible');
});
}
},
hideList: function (instant) {
const holder = this.closest('.field');
const list = holder.next().filter('.listbox, .treedropdown, .treemultiselect');
list.css('overflow', 'hidden');
if (instant) {
list.hide().css('display', 'none');
holder.removeClass('field--merge-below');
} else {
list.slideUp(() => {
holder.removeClass('field--merge-below');
});
}
},
});
});
var errorMessage = function(text) {
jQuery.noticeAdd({text: text, type: 'error', stayTime: 5000, inEffect: {left: '0', opacity: 'show'}});
};