[BUGFIX] Mark changes in `selectMultipleSideBySide`
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Public / JavaScript / FormEngine.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13
14 /**
15 * contains all JS functions related to TYPO3 TCEforms/FormEngine
16 *
17 * there are separate issues in this main object
18 * - functions, related to Element Browser ("Popup Window") and select fields
19 * - filling select fields (by wizard etc) from outside, formerly known via "setFormValueFromBrowseWin"
20 * - select fields: move selected items up and down via buttons, remove items etc
21 */
22
23 // add legacy functions to be accessible in the global scope
24 var setFormValueOpenBrowser,
25 setFormValueFromBrowseWin,
26 setHiddenFromList,
27 setFormValueManipulate,
28 setFormValue_getFObj;
29
30 /**
31 * Module: TYPO3/CMS/Backend/FormEngine
32 */
33 define(['jquery',
34 'TYPO3/CMS/Backend/FormEngineValidation',
35 'TYPO3/CMS/Backend/Modal',
36 'TYPO3/CMS/Backend/Severity',
37 'TYPO3/CMS/Backend/BackendException',
38 'TYPO3/CMS/Backend/Event/InteractionRequestMap'
39 ], function($, FormEngineValidation, Modal, Severity, BackendException, InteractionRequestMap) {
40
41 /**
42 * @param {InteractionRequest} interactionRequest
43 * @param {boolean} response
44 */
45 function handleConsumeResponse(interactionRequest, response) {
46 if (response) {
47 FormEngine.interactionRequestMap.resolveFor(interactionRequest);
48 } else {
49 FormEngine.interactionRequestMap.rejectFor(interactionRequest);
50 }
51 }
52
53 /**
54 * @exports TYPO3/CMS/Backend/FormEngine
55 */
56 var FormEngine = {
57 consumeTypes: ['typo3.setUrl', 'typo3.beforeSetUrl', 'typo3.refresh'],
58 Validation: FormEngineValidation,
59 interactionRequestMap: InteractionRequestMap,
60 formName: TYPO3.settings.FormEngine.formName,
61 openedPopupWindow: null,
62 legacyFieldChangedCb: function() {
63 !$.isFunction(TYPO3.settings.FormEngine.legacyFieldChangedCb) || TYPO3.settings.FormEngine.legacyFieldChangedCb();
64 },
65 browserUrl: ''
66 };
67
68 // functions to connect the db/file browser with this document and the formfields on it!
69
70 /**
71 * Opens a popup window with the element browser (browser.php)
72 *
73 * @param {String} mode can be "db" or "file"
74 * @param {String} params additional params for the browser window
75 */
76 FormEngine.openPopupWindow = setFormValueOpenBrowser = function(mode, params) {
77 Modal.advanced({
78 type: Modal.types.iframe,
79 content: FormEngine.browserUrl + '&mode=' + mode + '&bparams=' + params,
80 size: Modal.sizes.large
81 });
82 };
83
84 /**
85 * properly fills the select field from the popup window (element browser, link browser)
86 * or from a multi-select (two selects side-by-side)
87 * previously known as "setFormValueFromBrowseWin"
88 *
89 * @param {String} fieldName Formerly known as "fName" name of the field, like [tt_content][2387][header]
90 * @param {(String|Number)} value The value to fill in (could be an integer)
91 * @param {String} label The visible name in the selector
92 * @param {String} title The title when hovering over it
93 * @param {String} exclusiveValues If the select field has exclusive options that are not combine-able
94 * @param {$} $optionEl The jQuery object of the selected <option> tag
95 */
96 FormEngine.setSelectOptionFromExternalSource = setFormValueFromBrowseWin = function(fieldName, value, label, title, exclusiveValues, $optionEl) {
97 exclusiveValues = String(exclusiveValues);
98
99 var $fieldEl,
100 $originalFieldEl,
101 isMultiple = false,
102 isList = false;
103
104 $originalFieldEl = $fieldEl = FormEngine.getFieldElement(fieldName);
105
106 if ($originalFieldEl.length === 0 || value === '--div--') {
107 return;
108 }
109
110 // Check if the form object has a "_list" element
111 // The "_list" element exists for multiple selection select types
112 var $listFieldEl = FormEngine.getFieldElement(fieldName, '_list', true);
113 if ($listFieldEl.length > 0) {
114 $fieldEl = $listFieldEl;
115 isMultiple = ($fieldEl.prop('multiple') && $fieldEl.prop('size') != '1');
116 isList = true;
117 }
118
119 // clear field before adding value, if configured so (maxitems==1)
120 // @todo: clean this code
121 if (typeof TBE_EDITOR.clearBeforeSettingFormValueFromBrowseWin[fieldName] !== 'undefined') {
122 var clearSettings = TBE_EDITOR.clearBeforeSettingFormValueFromBrowseWin[fieldName];
123 $fieldEl.empty();
124
125 // Clear the upload field
126 // @todo: Investigate whether we either need to fix this code or we can drop it.
127 var filesContainer = document.getElementById(clearSettings.itemFormElID_file);
128 if (filesContainer) {
129 filesContainer.innerHTML = filesContainer.innerHTML;
130 }
131 }
132
133 if (isMultiple || isList) {
134 // If multiple values are not allowed, clear anything that is in the control already
135 if (!isMultiple) {
136 var $availableFieldEl = FormEngine.getFieldElement(fieldName, '_avail');
137 $fieldEl.find('option').each(function() {
138 $availableFieldEl
139 .find('option[value="' + $.escapeSelector($(this).attr('value')) + '"]')
140 .removeClass('hidden')
141 .prop('disabled', false);
142 });
143 $fieldEl.empty();
144 }
145
146 // Clear elements if exclusive values are found
147 if (exclusiveValues) {
148 var reenableOptions = false;
149
150 var m = new RegExp('(^|,)' + value + '($|,)');
151 // the new value is exclusive => remove all existing values
152 if (exclusiveValues.match(m)) {
153 $fieldEl.empty();
154 reenableOptions = true;
155 } else if ($fieldEl.find('option').length == 1) {
156 // there is an old value and it was exclusive => it has to be removed
157 m = new RegExp("(^|,)" + $fieldEl.find('option').prop('value') + "($|,)");
158 if (exclusiveValues.match(m)) {
159 $fieldEl.empty();
160 reenableOptions = true;
161 }
162 }
163
164 if (reenableOptions && typeof $optionEl !== 'undefined') {
165 $optionEl.closest('select').find('[disabled]').removeClass('hidden').prop('disabled', false)
166 }
167 }
168
169 // Inserting the new element
170 var addNewValue = true;
171
172 // check if there is a "_mul" field (a field on the right) and if the field was already added
173 var $multipleFieldEl = FormEngine.getFieldElement(fieldName, '_mul', true);
174 if ($multipleFieldEl.length == 0 || $multipleFieldEl.val() == 0) {
175 $fieldEl.find('option').each(function(k, optionEl) {
176 if ($(optionEl).prop('value') == value) {
177 addNewValue = false;
178 return false;
179 }
180 });
181
182 if (addNewValue && typeof $optionEl !== 'undefined') {
183 $optionEl.addClass('hidden').prop('disabled', true);
184 }
185 }
186
187 // element can be added
188 if (addNewValue) {
189 // finally add the option
190 var $option = $('<option></option>');
191 $option.attr({value: value, title: title}).text(label);
192 $option.appendTo($fieldEl);
193
194 // set the hidden field
195 FormEngine.updateHiddenFieldValueFromSelect($fieldEl, $originalFieldEl);
196
197 // execute the phpcode from $FormEngine->TBE_EDITOR_fieldChanged_func
198 FormEngine.legacyFieldChangedCb();
199 }
200
201 } else {
202
203 // The incoming value consists of the table name, an underscore and the uid
204 // or just the uid
205 // For a single selection field we need only the uid, so we extract it
206 var pattern = /_(\\d+)$/
207 , result = value.toString().match(pattern);
208
209 if (result != null) {
210 value = result[1];
211 }
212
213 // Change the selected value
214 $fieldEl.val(value);
215 }
216 if (typeof FormEngine.Validation !== 'undefined' && typeof FormEngine.Validation.validate === 'function') {
217 FormEngine.Validation.validate();
218 }
219 };
220
221 /**
222 * sets the value of the hidden field, from the select list, always executed after the select field was updated
223 * previously known as global function setHiddenFromList()
224 *
225 * @param {HTMLElement} selectFieldEl the select field
226 * @param {HTMLElement} originalFieldEl the hidden form field
227 */
228 FormEngine.updateHiddenFieldValueFromSelect = setHiddenFromList = function(selectFieldEl, originalFieldEl) {
229 var selectedValues = [];
230 $(selectFieldEl).find('option').each(function() {
231 selectedValues.push($(this).prop('value'));
232 });
233
234 // make a comma separated list, if it is a multi-select
235 // set the values to the final hidden field
236 $(originalFieldEl).val(selectedValues.join(','));
237 };
238
239 /**
240 * legacy function, can be removed once this function is not in use anymore
241 *
242 * @param {String} fName
243 * @param {String} type
244 * @param {Number} maxLength
245 */
246 setFormValueManipulate = function(fName, type, maxLength) {
247 var $formEl = FormEngine.getFormElement(fName);
248 if ($formEl.length > 0) {
249 var formObj = $formEl.get(0);
250 var localArray_V = [];
251 var localArray_L = [];
252 var localArray_S = [];
253 var localArray_T = [];
254 var fObjSel = formObj[fName + '_list'];
255 var l = fObjSel.length;
256 var c = 0;
257 var a;
258
259 if (type === 'RemoveFirstIfFull') {
260 if (maxLength == 1) {
261 for (a = 1; a < l; a++) {
262 if (fObjSel.options[a].selected != 1) {
263 localArray_V[c] = fObjSel.options[a].value;
264 localArray_L[c] = fObjSel.options[a].text;
265 localArray_S[c] = 0;
266 localArray_T[c] = fObjSel.options[a].title;
267 c++;
268 }
269 }
270 } else {
271 return;
272 }
273 }
274
275 if ((type === "Remove" && fObjSel.size > 1) || type === "Top" || type === "Bottom") {
276 if (type === "Top") {
277 for (a = 0; a < l; a++) {
278 if (fObjSel.options[a].selected == 1) {
279 localArray_V[c] = fObjSel.options[a].value;
280 localArray_L[c] = fObjSel.options[a].text;
281 localArray_S[c] = 1;
282 localArray_T[c] = fObjSel.options[a].title;
283 c++;
284 }
285 }
286 }
287 for (a = 0; a < l; a++) {
288 if (fObjSel.options[a].selected != 1) {
289 localArray_V[c] = fObjSel.options[a].value;
290 localArray_L[c] = fObjSel.options[a].text;
291 localArray_S[c] = 0;
292 localArray_T[c] = fObjSel.options[a].title;
293 c++;
294 }
295 }
296 if (type === "Bottom") {
297 for (a = 0; a < l; a++) {
298 if (fObjSel.options[a].selected == 1) {
299 localArray_V[c] = fObjSel.options[a].value;
300 localArray_L[c] = fObjSel.options[a].text;
301 localArray_S[c] = 1;
302 localArray_T[c] = fObjSel.options[a].title;
303 c++;
304 }
305 }
306 }
307 }
308 if (type === "Down") {
309 var tC = 0;
310 var tA = [];
311 var aa = 0;
312
313 for (a = 0; a < l; a++) {
314 if (fObjSel.options[a].selected != 1) {
315 // Add non-selected element:
316 localArray_V[c] = fObjSel.options[a].value;
317 localArray_L[c] = fObjSel.options[a].text;
318 localArray_S[c] = 0;
319 localArray_T[c] = fObjSel.options[a].title;
320 c++;
321
322 // Transfer any accumulated and reset:
323 if (tA.length > 0) {
324 for (aa = 0; aa < tA.length; aa++) {
325 localArray_V[c] = fObjSel.options[tA[aa]].value;
326 localArray_L[c] = fObjSel.options[tA[aa]].text;
327 localArray_S[c] = 1;
328 localArray_T[c] = fObjSel.options[tA[aa]].title;
329 c++;
330 }
331
332 tC = 0;
333 tA = [];
334 }
335 } else {
336 tA[tC] = a;
337 tC++;
338 }
339 }
340 // Transfer any remaining:
341 if (tA.length > 0) {
342 for (aa = 0; aa < tA.length; aa++) {
343 localArray_V[c] = fObjSel.options[tA[aa]].value;
344 localArray_L[c] = fObjSel.options[tA[aa]].text;
345 localArray_S[c] = 1;
346 localArray_T[c] = fObjSel.options[tA[aa]].title;
347 c++;
348 }
349 }
350 }
351 if (type === "Up") {
352 var tC = 0;
353 var tA = [];
354 var aa = 0;
355 c = l - 1;
356
357 for (a = l - 1; a >= 0; a--) {
358 if (fObjSel.options[a].selected != 1) {
359
360 // Add non-selected element:
361 localArray_V[c] = fObjSel.options[a].value;
362 localArray_L[c] = fObjSel.options[a].text;
363 localArray_S[c] = 0;
364 localArray_T[c] = fObjSel.options[a].title;
365 c--;
366
367 // Transfer any accumulated and reset:
368 if (tA.length > 0) {
369 for (aa = 0; aa < tA.length; aa++) {
370 localArray_V[c] = fObjSel.options[tA[aa]].value;
371 localArray_L[c] = fObjSel.options[tA[aa]].text;
372 localArray_S[c] = 1;
373 localArray_T[c] = fObjSel.options[tA[aa]].title;
374 c--;
375 }
376
377 tC = 0;
378 tA = [];
379 }
380 } else {
381 tA[tC] = a;
382 tC++;
383 }
384 }
385 // Transfer any remaining:
386 if (tA.length > 0) {
387 for (aa = 0; aa < tA.length; aa++) {
388 localArray_V[c] = fObjSel.options[tA[aa]].value;
389 localArray_L[c] = fObjSel.options[tA[aa]].text;
390 localArray_S[c] = 1;
391 localArray_T[c] = fObjSel.options[tA[aa]].title;
392 c--;
393 }
394 }
395 c = l; // Restore length value in "c"
396 }
397
398 // Transfer items in temporary storage to list object:
399 fObjSel.length = c;
400 for (a = 0; a < c; a++) {
401 fObjSel.options[a].value = localArray_V[a];
402 fObjSel.options[a].text = localArray_L[a];
403 fObjSel.options[a].selected = localArray_S[a];
404 fObjSel.options[a].title = localArray_T[a];
405 }
406 FormEngine.updateHiddenFieldValueFromSelect(fObjSel, formObj[fName]);
407
408 FormEngine.legacyFieldChangedCb();
409 }
410 };
411
412
413 /**
414 * Legacy function
415 * returns the DOM object for the given form name of the current form,
416 * but only if the given field name is valid, legacy function, use "getFormElement" instead
417 *
418 * @param {String} fieldName the name of the field name
419 * @returns {*|HTMLElement}
420 */
421 setFormValue_getFObj = function(fieldName) {
422 var $formEl = FormEngine.getFormElement(fieldName);
423 if ($formEl.length > 0) {
424 // return the DOM element of the form object
425 return $formEl.get(0);
426 }
427 return null;
428 };
429
430 /**
431 * returns a jQuery object for the given form name of the current form,
432 * if the parameter "fieldName" is given, then the form element is only returned if the field name is available
433 * the latter behaviour mirrors the one of the function "setFormValue_getFObj"
434 *
435 * @param {String} fieldName the field name to check for, optional
436 * @returns {*|HTMLElement}
437 */
438 FormEngine.getFormElement = function(fieldName) {
439 var $formEl = $('form[name="' + FormEngine.formName + '"]:first');
440 if (fieldName) {
441 var $fieldEl = FormEngine.getFieldElement(fieldName)
442 , $listFieldEl = FormEngine.getFieldElement(fieldName, '_list');
443
444 // Take the form object if it is either of type select-one or of type-multiple and it has a "_list" element
445 if ($fieldEl.length > 0 &&
446 (
447 ($fieldEl.prop('type') === 'select-one') ||
448 ($listFieldEl.length > 0 && $listFieldEl.prop('type').match(/select-(one|multiple)/))
449 )
450 ) {
451 return $formEl;
452 } else {
453 console.error('Form fields missing: form: ' + FormEngine.formName + ', field name: ' + fieldName);
454 alert('Form field is invalid');
455 }
456 } else {
457 return $formEl;
458 }
459 };
460
461
462 /**
463 * Returns a jQuery object of the field DOM element of the current form, can also be used to
464 * request an alternative field like "_hr", "_list" or "_mul"
465 *
466 * @param {String} fieldName the name of the field (<input name="fieldName">)
467 * @param {String} appendix optional
468 * @param {Boolean} noFallback if set, then the appendix value is returned no matter if it exists or not
469 * @returns {*|HTMLElement}
470 */
471 FormEngine.getFieldElement = function(fieldName, appendix, noFallback) {
472 var $formEl = FormEngine.getFormElement();
473
474 // if an appendix is set, return the field with the appendix (like _mul or _list)
475 if (appendix) {
476 var $fieldEl;
477 switch (appendix) {
478 case '_list':
479 $fieldEl = $(':input.tceforms-multiselect[data-formengine-input-name="' + fieldName + '"]', $formEl);
480 break;
481 case '_avail':
482 $fieldEl = $(':input[data-relatedfieldname="' + fieldName + '"]', $formEl);
483 break;
484 case '_mul':
485 case '_hr':
486 $fieldEl = $(':input[type=hidden][data-formengine-input-name="' + fieldName + '"]', $formEl);
487 break;
488 }
489 if (($fieldEl && $fieldEl.length > 0) || noFallback === true) {
490 return $fieldEl;
491 }
492 }
493
494 return $(':input[name="' + fieldName + '"]', $formEl);
495 };
496
497
498 /**************************************************
499 * manipulate existing options in a select field
500 **************************************************/
501
502 /**
503 * Moves currently selected options from a select field to the very top,
504 * can be multiple entries as well
505 *
506 * @param {Object} $fieldEl a jQuery object, containing the select field
507 */
508 FormEngine.moveOptionToTop = function($fieldEl) {
509 // remove the selected options
510 var selectedOptions = $fieldEl.find(':selected').detach();
511 // and add them on first position again
512 $fieldEl.prepend(selectedOptions);
513 };
514
515
516 /**
517 * moves currently selected options from a select field up by one position,
518 * can be multiple entries as well
519 *
520 * @param {Object} $fieldEl a jQuery object, containing the select field
521 */
522 FormEngine.moveOptionUp = function($fieldEl) {
523 // remove the selected options and add it before the previous sibling
524 $.each($fieldEl.find(':selected'), function(k, optionEl) {
525 var $optionEl = $(optionEl)
526 , $optionBefore = $optionEl.prev();
527
528 // stop if first option to move is already the first one
529 if (k == 0 && $optionBefore.length === 0) {
530 return false;
531 }
532
533 $optionBefore.before($optionEl.detach());
534 });
535 };
536
537
538 /**
539 * moves currently selected options from a select field down one position,
540 * can be multiple entries as well
541 *
542 * @param {Object} $fieldEl a jQuery object, containing the select field
543 */
544 FormEngine.moveOptionDown = function($fieldEl) {
545 // remove the selected options and add it after the next sibling
546 // however, this time, we need to go from the last to the first
547 var selectedOptions = $fieldEl.find(':selected');
548 selectedOptions = $.makeArray(selectedOptions);
549 selectedOptions.reverse();
550 $.each(selectedOptions, function(k, optionEl) {
551 var $optionEl = $(optionEl)
552 , $optionAfter = $optionEl.next();
553
554 // stop if first option to move is already the last one
555 if (k == 0 && $optionAfter.length === 0) {
556 return false;
557 }
558
559 $optionAfter.after($optionEl.detach());
560 });
561 };
562
563
564 /**
565 * moves currently selected options from a select field as the very last entries
566 *
567 * @param {Object} $fieldEl a jQuery object, containing the select field
568 */
569 FormEngine.moveOptionToBottom = function($fieldEl) {
570 // remove the selected options
571 var selectedOptions = $fieldEl.find(':selected').detach();
572 // and add them on last position again
573 $fieldEl.append(selectedOptions);
574 };
575
576 /**
577 * removes currently selected options from a select field
578 *
579 * @param {Object} $fieldEl a jQuery object, containing the select field
580 * @param {Object} $availableFieldEl a jQuery object, containing all available value
581 */
582 FormEngine.removeOption = function($fieldEl, $availableFieldEl) {
583 var $selected = $fieldEl.find(':selected');
584
585 $selected.each(function() {
586 $availableFieldEl
587 .find('option[value="' + $.escapeSelector($(this).attr('value')) + '"]')
588 .removeClass('hidden')
589 .prop('disabled', false);
590 });
591
592 // remove the selected options
593 $selected.remove();
594 };
595
596
597 /**
598 * Initialize events for all form engine relevant tasks.
599 * This function only needs to be called once on page load,
600 * as it using deferrer methods only
601 */
602 FormEngine.initializeEvents = function() {
603 if (top.TYPO3 && typeof top.TYPO3.Backend !== 'undefined') {
604 top.TYPO3.Backend.consumerScope.attach(FormEngine);
605 $(window).on('unload', function() {
606 top.TYPO3.Backend.consumerScope.detach(FormEngine);
607 });
608 }
609 $(document).on('click', '.t3js-btn-moveoption-top, .t3js-btn-moveoption-up, .t3js-btn-moveoption-down, .t3js-btn-moveoption-bottom, .t3js-btn-removeoption', function(evt) {
610 evt.preventDefault();
611
612 // track the arrows "Up", "Down", "Clear" etc in multi-select boxes
613 var $el = $(this)
614 , fieldName = $el.data('fieldname')
615 , $listFieldEl = FormEngine.getFieldElement(fieldName, '_list');
616
617 if ($listFieldEl.length > 0) {
618
619 if ($el.hasClass('t3js-btn-moveoption-top')) {
620 FormEngine.moveOptionToTop($listFieldEl);
621 } else if ($el.hasClass('t3js-btn-moveoption-up')) {
622 FormEngine.moveOptionUp($listFieldEl);
623 } else if ($el.hasClass('t3js-btn-moveoption-down')) {
624 FormEngine.moveOptionDown($listFieldEl);
625 } else if ($el.hasClass('t3js-btn-moveoption-bottom')) {
626 FormEngine.moveOptionToBottom($listFieldEl);
627 } else if ($el.hasClass('t3js-btn-removeoption')) {
628 var $availableFieldEl = FormEngine.getFieldElement(fieldName, '_avail');
629 FormEngine.removeOption($listFieldEl, $availableFieldEl);
630 }
631
632 // make sure to update the hidden field value when modifying the select value
633 FormEngine.updateHiddenFieldValueFromSelect($listFieldEl, FormEngine.getFieldElement(fieldName));
634 FormEngine.legacyFieldChangedCb();
635 if (typeof FormEngine.Validation !== 'undefined' && typeof FormEngine.Validation.validate === 'function') {
636 FormEngine.Validation.markFieldAsChanged($listFieldEl);
637 FormEngine.Validation.validate();
638 }
639 }
640 }).on('click', '.t3js-formengine-select-itemstoselect', function(evt) {
641 // in multi-select environments with two (e.g. "Access"), on click the item from the right should go to the left
642 var $el = $(this)
643 , fieldName = $el.data('relatedfieldname')
644 , exclusiveValues = $el.data('exclusivevalues');
645
646 if (fieldName) {
647 // try to add each selected field to the "left" select field
648 $el.find(':selected').each(function() {
649 var $optionEl = $(this);
650 FormEngine.setSelectOptionFromExternalSource(fieldName, $optionEl.prop('value'), $optionEl.text(), $optionEl.prop('title'), exclusiveValues, $optionEl);
651 });
652 }
653 }).on('click', '.t3js-editform-close', function(e) {
654 e.preventDefault();
655 FormEngine.preventExitIfNotSaved(
656 FormEngine.preventExitIfNotSavedCallback
657 );
658 }).on('click', '.t3js-editform-view', function(e) {
659 e.preventDefault();
660 FormEngine.previewAction(e, FormEngine.previewActionCallback);
661 }).on('click', '.t3js-editform-new', function(e) {
662 e.preventDefault();
663 FormEngine.newAction(e, FormEngine.newActionCallback);
664 }).on('click', '.t3js-editform-duplicate', function(e) {
665 e.preventDefault();
666 FormEngine.duplicateAction(e, FormEngine.duplicateActionCallback);
667 }).on('click', '.t3js-editform-delete-record', function(e) {
668 e.preventDefault();
669 FormEngine.deleteAction(e, FormEngine.deleteActionCallback);
670 }).on('click', '.t3js-editform-delete-inline-record', function(e) {
671 e.preventDefault();
672 var title = TYPO3.lang['label.confirm.delete_record.title'] || 'Delete this record?';
673 var content = TYPO3.lang['label.confirm.delete_record.content'] || 'Are you sure you want to delete this record?';
674 var $anchorElement = $(this);
675 var $modal = Modal.confirm(title, content, Severity.warning, [
676 {
677 text: TYPO3.lang['buttons.confirm.delete_record.no'] || 'Cancel',
678 active: true,
679 btnClass: 'btn-default',
680 name: 'no'
681 },
682 {
683 text: TYPO3.lang['buttons.confirm.delete_record.yes'] || 'Yes, delete this record',
684 btnClass: 'btn-warning',
685 name: 'yes'
686 }
687 ]);
688 $modal.on('button.clicked', function(e) {
689 if (e.target.name === 'no') {
690 Modal.dismiss();
691 } else if (e.target.name === 'yes') {
692 var objectId = $anchorElement.data('objectid');
693 inline.deleteRecord(objectId);
694 Modal.dismiss();
695 }
696 });
697 }).on('click', '.t3js-editform-submitButton', function(event) {
698 // remember the clicked submit button. we need to know that in TBE_EDITOR.submitForm();
699 var $me = $(this),
700 name = $me.data('name') || this.name,
701 $elem = $('<input />').attr('type', 'hidden').attr('name', name).attr('value', '1');
702
703 $me.parents('form').append($elem);
704 }).on('change', '.t3-form-field-eval-null-checkbox input[type="checkbox"]', function(e) {
705 // Null checkboxes without placeholder click event handler
706 $(this).closest('.t3js-formengine-field-item').toggleClass('disabled');
707 }).on('change', '.t3js-form-field-eval-null-placeholder-checkbox input[type="checkbox"]', function(e) {
708 FormEngine.toggleCheckboxField($(this));
709 }).on('change', '.t3js-l10n-state-container input[type=radio]', function(event) {
710 // Change handler for "l10n_state" field changes
711 var $me = $(this);
712 var $input = $me.closest('.t3js-formengine-field-item').find('[data-formengine-input-name]');
713
714 if ($input.length > 0) {
715 var lastState = $input.data('last-l10n-state') || false,
716 currentState = $(this).val();
717
718 if (lastState && currentState === lastState) {
719 return;
720 }
721
722 if (currentState === 'custom') {
723 if (lastState) {
724 $(this).attr('data-original-language-value', $input.val());
725 }
726 $input.attr('disabled', false);
727 } else {
728 if (lastState === 'custom') {
729 $(this).closest('.t3js-l10n-state-container').find('.t3js-l10n-state-custom').attr('data-original-language-value', $input.val());
730 }
731 $input.attr('disabled', 'disabled');
732 }
733
734 $input.val($(this).attr('data-original-language-value')).trigger('change');
735 $input.data('last-l10n-state', $(this).val());
736 }
737 }).on('formengine.dp.change', function(event, $field) {
738 FormEngine.Validation.validate();
739 FormEngine.Validation.markFieldAsChanged($field);
740 $('.module-docheader-bar .btn').removeClass('disabled').prop('disabled', false);
741 }).on('change', function(event) {
742 $('.module-docheader-bar .btn').removeClass('disabled').prop('disabled', false);
743 });
744 };
745
746 /**
747 * @param {InteractionRequest} interactionRequest
748 * @return {jQuery.Deferred}
749 */
750 FormEngine.consume = function(interactionRequest) {
751 if (!interactionRequest) {
752 throw new BackendException('No interaction request given', 1496589980);
753 }
754 if (interactionRequest.concernsTypes(FormEngine.consumeTypes)) {
755 var outerMostRequest = interactionRequest.outerMostRequest;
756 var deferred = $.Deferred();
757
758 FormEngine.interactionRequestMap.attachFor(
759 outerMostRequest,
760 deferred
761 );
762 // resolve or reject deferreds with previous user choice
763 if (outerMostRequest.isProcessed()) {
764 handleConsumeResponse(
765 outerMostRequest,
766 outerMostRequest.getProcessedData().response
767 );
768 // show confirmation dialog
769 } else if (FormEngine.hasChange()) {
770 FormEngine.preventExitIfNotSaved(function(response) {
771 outerMostRequest.setProcessedData(
772 {response: response}
773 );
774 handleConsumeResponse(outerMostRequest, response);
775 });
776 // resolve directly
777 } else {
778 FormEngine.interactionRequestMap.resolveFor(outerMostRequest);
779 }
780
781 return deferred;
782 }
783 };
784
785 /**
786 * Initializes the remaining character views based on the fields' maxlength attribute
787 */
788 FormEngine.initializeRemainingCharacterViews = function() {
789 // all fields with a "maxlength" attribute
790 var $maxlengthElements = $('[maxlength]').not('.t3js-datetimepicker').not('.t3js-charcounter-initialized');
791 $maxlengthElements.on('focus', function(e) {
792 var $field = $(this),
793 $parent = $field.parents('.t3js-formengine-field-item:first'),
794 maxlengthProperties = FormEngine.getCharacterCounterProperties($field);
795
796 // append the counter only at focus to avoid cluttering the DOM
797 $parent.append($('<div />', {'class': 't3js-charcounter'}).append(
798 $('<span />', {'class': maxlengthProperties.labelClass}).text(TYPO3.lang['FormEngine.remainingCharacters'].replace('{0}', maxlengthProperties.remainingCharacters))
799 ));
800 }).on('blur', function() {
801 var $field = $(this),
802 $parent = $field.parents('.t3js-formengine-field-item:first');
803 $parent.find('.t3js-charcounter').remove();
804 }).on('keyup', function() {
805 var $field = $(this),
806 $parent = $field.parents('.t3js-formengine-field-item:first'),
807 maxlengthProperties = FormEngine.getCharacterCounterProperties($field);
808
809 // change class and value
810 $parent.find('.t3js-charcounter span').removeClass().addClass(maxlengthProperties.labelClass).text(TYPO3.lang['FormEngine.remainingCharacters'].replace('{0}', maxlengthProperties.remainingCharacters))
811 });
812 $maxlengthElements.addClass('t3js-charcounter-initialized');
813 $(':password').on('focus', function() {
814 $(this).attr('type', 'text').select();
815 }).on('blur', function() {
816 $(this).attr('type', 'password');
817 });
818 };
819
820 /**
821 * Initialize select checkbox element checkboxes
822 */
823 FormEngine.initializeSelectCheckboxes = function() {
824 $('.t3js-toggle-checkboxes').each(function() {
825 var $checkbox = $(this);
826 var $table = $checkbox.closest('table');
827 var $checkboxes = $table.find('.t3js-checkbox');
828 var checkIt = $checkboxes.length === $table.find('.t3js-checkbox:checked').length;
829 $checkbox.prop('checked', checkIt);
830 });
831 $(document).on('change', '.t3js-toggle-checkboxes', function(e) {
832 e.preventDefault();
833 var $checkbox = $(this);
834 var $table = $checkbox.closest('table');
835 var $checkboxes = $table.find('.t3js-checkbox');
836 var checkIt = $checkboxes.length !== $table.find('.t3js-checkbox:checked').length;
837 $checkboxes.prop('checked', checkIt);
838 $checkbox.prop('checked', checkIt);
839 FormEngine.Validation.markFieldAsChanged($checkbox);
840 });
841 $(document).on('change', '.t3js-checkbox', function(e) {
842 FormEngine.updateCheckboxState(this);
843 });
844 };
845
846 /**
847 *
848 * @param {HTMLElement} source
849 */
850 FormEngine.updateCheckboxState = function(source) {
851 var $sourceElement = $(source);
852 var $table = $sourceElement.closest('table');
853 var $checkboxes = $table.find('.t3js-checkbox');
854 var checkIt = $checkboxes.length === $table.find('.t3js-checkbox:checked').length;
855 $table.find('.t3js-toggle-checkboxes').prop('checked', checkIt);
856 };
857
858 /**
859 * Get the properties required for proper rendering of the character counter
860 *
861 * @param {Object} $field
862 * @returns {{remainingCharacters: number, labelClass: string}}
863 */
864 FormEngine.getCharacterCounterProperties = function($field) {
865 var fieldText = $field.val(),
866 maxlength = $field.attr('maxlength'),
867 currentFieldLength = fieldText.length,
868 numberOfLineBreaks = (fieldText.match(/\n/g) || []).length, // count line breaks
869 remainingCharacters = maxlength - currentFieldLength - numberOfLineBreaks,
870 threshold = 15, // hard limit of remaining characters when the label class changes
871 labelClass = '';
872
873 if (remainingCharacters < threshold) {
874 labelClass = 'label-danger';
875 } else if (remainingCharacters < threshold * 2) {
876 labelClass = 'label-warning';
877 } else {
878 labelClass = 'label-info';
879 }
880
881 return {
882 remainingCharacters: remainingCharacters,
883 labelClass: 'label ' + labelClass
884 };
885 };
886
887 /**
888 * Select field filter functions, see TCA option "enableMultiSelectFilterTextfield"
889 * and "multiSelectFilterItems"
890 */
891 FormEngine.SelectBoxFilter = {
892 options: {
893 fieldContainerSelector: '.t3js-formengine-field-group',
894 filterContainerSelector: '.t3js-formengine-multiselect-filter-container',
895 filterTextFieldSelector: '.t3js-formengine-multiselect-filter-textfield',
896 filterSelectFieldSelector: '.t3js-formengine-multiselect-filter-dropdown',
897 itemsToSelectElementSelector: '.t3js-formengine-select-itemstoselect'
898 }
899 };
900
901 /**
902 * Make sure that all selectors and input filters are recognized
903 * note: this also works on elements that are loaded asynchronously via AJAX, no need to call this method
904 * after an AJAX load.
905 */
906 FormEngine.SelectBoxFilter.initializeEvents = function() {
907 $(document).on('keyup', FormEngine.SelectBoxFilter.options.filterTextFieldSelector, function() {
908 var $selectElement = FormEngine.SelectBoxFilter.getSelectElement($(this));
909 FormEngine.SelectBoxFilter.filter($selectElement, $(this).val());
910 }).on('change', FormEngine.SelectBoxFilter.options.filterSelectFieldSelector, function() {
911 var $selectElement = FormEngine.SelectBoxFilter.getSelectElement($(this));
912 FormEngine.SelectBoxFilter.filter($selectElement, $(this).val());
913 });
914 };
915
916 /**
917 * Fetch the "itemstoselect" select element where a filter item is attached to
918 *
919 * @param {Object} $relativeElement
920 * @returns {*}
921 */
922 FormEngine.SelectBoxFilter.getSelectElement = function($relativeElement) {
923 var $containerElement = $relativeElement.closest(FormEngine.SelectBoxFilter.options.fieldContainerSelector);
924 return $containerElement.find(FormEngine.SelectBoxFilter.options.itemsToSelectElementSelector);
925 };
926
927 /**
928 * Filter the actual items
929 *
930 * @param {Object} $selectElement
931 * @param {String} filterText
932 */
933 FormEngine.SelectBoxFilter.filter = function($selectElement, filterText) {
934 var $allOptionElements;
935 if (!$selectElement.data('alloptions')) {
936 $allOptionElements = $selectElement.find('option').clone();
937 $selectElement.data('alloptions', $allOptionElements);
938 } else {
939 $allOptionElements = $selectElement.data('alloptions');
940 }
941
942 if (filterText.length > 0) {
943 var matchFilter = new RegExp(filterText, 'i');
944 $selectElement.html('');
945 $allOptionElements.each(function() {
946 var $item = $(this);
947 if ($item.text().match(matchFilter)) {
948 $selectElement.append($item.clone());
949 }
950 });
951 } else {
952 $selectElement.html($allOptionElements);
953 }
954 };
955
956 /**
957 * convert all textareas so they grow when it is typed in.
958 */
959 FormEngine.convertTextareasResizable = function() {
960 var $elements = $('.t3js-formengine-textarea');
961 if (TYPO3.settings.Textarea && TYPO3.settings.Textarea.autosize && $elements.length) {
962 require(['autosize'], function(autosize) {
963 autosize($elements);
964 });
965 }
966 };
967
968 /**
969 * convert all textareas to enable tab
970 */
971 FormEngine.convertTextareasEnableTab = function() {
972 var $elements = $('.t3js-enable-tab');
973 if ($elements.length) {
974 require(['taboverride'], function(taboverride) {
975 taboverride.set($elements);
976 });
977 }
978 };
979
980 /**
981 * Initialize input / text field "null" checkbox CSS overlay if no placeholder is set.
982 */
983 FormEngine.initializeNullNoPlaceholderCheckboxes = function() {
984 $('.t3-form-field-eval-null-checkbox').each(function() {
985 // Add disabled class to "t3js-formengine-field-item" if the null checkbox is NOT set,
986 // This activates a CSS overlay "disabling" the input field and everything around.
987 var $checkbox = $(this).find('input[type="checkbox"]');
988 var $fieldItem = $(this).closest('.t3js-formengine-field-item');
989 if (!$checkbox.attr('checked')) {
990 $fieldItem.addClass('disabled');
991 }
992 });
993 };
994
995 /**
996 * Initialize input / text field "null" checkbox placeholder / real field if placeholder is set.
997 */
998 FormEngine.initializeNullWithPlaceholderCheckboxes = function() {
999 $('.t3js-form-field-eval-null-placeholder-checkbox').each(function() {
1000 FormEngine.toggleCheckboxField($(this).find('input[type="checkbox"]'));
1001 });
1002 };
1003
1004 /**
1005 * Set initial state of both div's (one containing actual field, other containing placeholder field)
1006 * depending on whether checkbox is checked or not
1007 * @param $checkbox
1008 */
1009 FormEngine.toggleCheckboxField = function($checkbox) {
1010 var $item = $checkbox.closest('.t3js-formengine-field-item');
1011 if ($checkbox.prop('checked')) {
1012 $item.find('.t3js-formengine-placeholder-placeholder').hide();
1013 $item.find('.t3js-formengine-placeholder-formfield').show();
1014 } else {
1015 $item.find('.t3js-formengine-placeholder-placeholder').show();
1016 $item.find('.t3js-formengine-placeholder-formfield').hide();
1017 }
1018 };
1019
1020 /**
1021 * This is the main function that is called on page load, but also after elements are asynchronously
1022 * called e.g. after inline elements are loaded, or a new flexform section is added.
1023 * Use this function in your extension like this "TYPO3.FormEngine.initialize()"
1024 * if you add new fields dynamically.
1025 */
1026 FormEngine.reinitialize = function() {
1027 // Apply "close" button to all input / datetime fields
1028 if ($('.t3js-clearable').length) {
1029 require(['TYPO3/CMS/Backend/jquery.clearable'], function() {
1030 $('.t3js-clearable').clearable();
1031 });
1032 }
1033 if ($('.t3-form-suggest').length) {
1034 require(['TYPO3/CMS/Backend/FormEngineSuggest'], function(Suggest) {
1035 $('.t3-form-suggest').each(function(index, suggestElement) {
1036 new Suggest(suggestElement);
1037 });
1038 });
1039 }
1040 // Apply DatePicker to all date time fields
1041 if ($('.t3js-datetimepicker').length) {
1042 require(['TYPO3/CMS/Backend/DateTimePicker'], function(DateTimePicker) {
1043 DateTimePicker.initialize();
1044 });
1045 }
1046
1047 FormEngine.convertTextareasResizable();
1048 FormEngine.convertTextareasEnableTab();
1049 FormEngine.initializeNullNoPlaceholderCheckboxes();
1050 FormEngine.initializeNullWithPlaceholderCheckboxes();
1051 FormEngine.initializeInputLinkToggle();
1052 FormEngine.initializeLocalizationStateSelector();
1053 FormEngine.initializeRemainingCharacterViews();
1054 };
1055
1056 /**
1057 * Disable the input field on load if localization state selector is set to "parent" or "source"
1058 */
1059 FormEngine.initializeLocalizationStateSelector = function() {
1060 $('.t3js-l10n-state-container').each(function() {
1061 var $input = $(this).closest('.t3js-formengine-field-item').find('[data-formengine-input-name]');
1062 var currentState = $(this).find('input[type="radio"]:checked').val();
1063 if (currentState === 'parent' || currentState === 'source') {
1064 $input.attr('disabled', 'disabled');
1065 }
1066 });
1067 };
1068
1069 /**
1070 * Toggle for input link explanation
1071 */
1072 FormEngine.initializeInputLinkToggle = function() {
1073 var toggleClass = '.t3js-form-field-inputlink-explanation-toggle',
1074 inputFieldClass = '.t3js-form-field-inputlink-input',
1075 explanationClass = '.t3js-form-field-inputlink-explanation';
1076
1077 // if empty, show input field
1078 $(explanationClass).filter(function() {
1079 return !$.trim($(this).val());
1080 }).each(function() {
1081 var $group = $(this).closest('.t3js-form-field-inputlink'),
1082 $inputField = $group.find(inputFieldClass),
1083 $explanationField = $group.find(explanationClass);
1084 $explanationField.toggleClass('hidden', true);
1085 $inputField.toggleClass('hidden', false);
1086 $group.find('.form-control-clearable button.close').toggleClass('hidden', false)
1087 });
1088
1089 $(document).on('click', toggleClass, function(e) {
1090 e.preventDefault();
1091
1092 var $group = $(this).closest('.t3js-form-field-inputlink'),
1093 $inputField = $group.find(inputFieldClass),
1094 $explanationField = $group.find(explanationClass),
1095 explanationShown;
1096
1097 explanationShown = !$explanationField.hasClass('hidden');
1098 $explanationField.toggleClass('hidden', explanationShown);
1099 $inputField.toggleClass('hidden', !explanationShown);
1100 $group.find('.form-control-clearable button.close').toggleClass('hidden', !explanationShown)
1101 });
1102
1103 $(inputFieldClass).on('change', function() {
1104 var $group = $(this).closest('.t3js-form-field-inputlink'),
1105 $inputField = $group.find(inputFieldClass),
1106 $explanationField = $group.find(explanationClass),
1107 explanationShown;
1108
1109 if (!$explanationField.hasClass('hidden')) {
1110
1111 explanationShown = !$explanationField.hasClass('hidden');
1112 $explanationField.toggleClass('hidden', explanationShown);
1113 $inputField.toggleClass('hidden', !explanationShown);
1114 $group.find('.form-control-clearable button.close').toggleClass('hidden', !explanationShown)
1115 }
1116 });
1117 };
1118
1119 /**
1120 * @return {boolean}
1121 */
1122 FormEngine.hasChange = function() {
1123 var formElementChanges = $('form[name="' + FormEngine.formName + '"] .has-change').length > 0,
1124 inlineRecordChanges = $('[name^="data["].has-change').length > 0;
1125 return formElementChanges || inlineRecordChanges;
1126 };
1127
1128 /**
1129 * @param {boolean} response
1130 */
1131 FormEngine.preventExitIfNotSavedCallback = function(response) {
1132 FormEngine.closeDocument();
1133 };
1134
1135 /**
1136 * Show modal to confirm following a clicked link to confirm leaving the document without saving
1137 *
1138 * @param {String} href
1139 * @returns {Boolean}
1140 */
1141 FormEngine.preventFollowLinkIfNotSaved = function(href) {
1142 FormEngine.preventExitIfNotSaved(
1143 function () {
1144 window.location.href = href;
1145 }
1146 );
1147 return false;
1148 };
1149
1150 /**
1151 * Show modal to confirm closing the document without saving.
1152 *
1153 * @param {Function} callback
1154 */
1155 FormEngine.preventExitIfNotSaved = function(callback) {
1156 callback = callback || FormEngine.preventExitIfNotSavedCallback;
1157
1158 if (FormEngine.hasChange()) {
1159 var title = TYPO3.lang['label.confirm.close_without_save.title'] || 'Do you want to close without saving?';
1160 var content = TYPO3.lang['label.confirm.close_without_save.content'] || 'You currently have unsaved changes. Are you sure you want to discard these changes?';
1161 var $elem = $('<input />').attr('type', 'hidden').attr('name', '_saveandclosedok').attr('value', '1');
1162 var buttons = [
1163 {
1164 text: TYPO3.lang['buttons.confirm.close_without_save.no'] || 'No, I will continue editing',
1165 btnClass: 'btn-default',
1166 name: 'no'
1167 },
1168 {
1169 text: TYPO3.lang['buttons.confirm.close_without_save.yes'] || 'Yes, discard my changes',
1170 btnClass: 'btn-default',
1171 name: 'yes'
1172 }
1173 ];
1174 if ($('.has-error').length === 0) {
1175 buttons.push({
1176 text: TYPO3.lang['buttons.confirm.save_and_close'] || 'Save and close',
1177 btnClass: 'btn-warning',
1178 name: 'save',
1179 active: true
1180 });
1181 }
1182
1183 var $modal = Modal.confirm(title, content, Severity.warning, buttons);
1184 $modal.on('button.clicked', function(e) {
1185 if (e.target.name === 'no') {
1186 Modal.dismiss();
1187 } else if (e.target.name === 'yes') {
1188 Modal.dismiss();
1189 callback.call(null, true);
1190 } else if (e.target.name === 'save') {
1191 $('form[name=' + FormEngine.formName + ']').append($elem);
1192 $('input[name=doSave]').val(1);
1193 Modal.dismiss();
1194 document.editform.submit();
1195 }
1196 });
1197 } else {
1198 callback.call(null, true);
1199 }
1200 };
1201
1202 /**
1203 * Show modal to confirm closing the document without saving
1204 */
1205 FormEngine.preventSaveIfHasErrors = function() {
1206 if ($('.has-error').length > 0) {
1207 var title = TYPO3.lang['label.alert.save_with_error.title'] || 'You have errors in your form!';
1208 var content = TYPO3.lang['label.alert.save_with_error.content'] || 'Please check the form, there is at least one error in your form.';
1209 var $modal = Modal.confirm(title, content, Severity.error, [
1210 {
1211 text: TYPO3.lang['buttons.alert.save_with_error.ok'] || 'OK',
1212 btnClass: 'btn-danger',
1213 name: 'ok'
1214 }
1215 ]);
1216 $modal.on('button.clicked', function(e) {
1217 if (e.target.name === 'ok') {
1218 Modal.dismiss();
1219 }
1220 });
1221 return false;
1222 }
1223 return true;
1224 };
1225
1226 /**
1227 * Preview action
1228 *
1229 * When there are changes:
1230 * Will take action based on local storage preset
1231 * If preset is not available, a modal will open
1232 *
1233 * @param {Event} event
1234 * @param {Function} callback
1235 */
1236 FormEngine.previewAction = function(event, callback) {
1237 callback = callback || FormEngine.previewActionCallback;
1238
1239 var previewUrl = event.target.href;
1240 var isNew = event.target.dataset.hasOwnProperty('isNew');
1241 var $actionElement = $('<input />').attr('type', 'hidden').attr('name', '_savedokview').attr('value', '1');
1242 if (FormEngine.hasChange()) {
1243 FormEngine.showPreviewModal(previewUrl, isNew, $actionElement, callback);
1244 } else {
1245 $('form[name=' + FormEngine.formName + ']').append($actionElement);
1246 window.open('', 'newTYPO3frontendWindow');
1247 document.editform.submit();
1248 }
1249 };
1250
1251 /**
1252 * The callback for the preview action
1253 *
1254 * @param {string} modalButtonName
1255 * @param {string} previewUrl
1256 * @param {element} $actionElement
1257 */
1258 FormEngine.previewActionCallback = function(modalButtonName, previewUrl, $actionElement) {
1259 Modal.dismiss();
1260 switch(modalButtonName) {
1261 case 'discard':
1262 var previewWin = window.open(previewUrl, 'newTYPO3frontendWindow');
1263 previewWin.focus();
1264 if (previewWin.location.href === previewUrl) {
1265 previewWin.location.reload();
1266 }
1267 break;
1268 case 'save':
1269 $('form[name=' + FormEngine.formName + ']').append($actionElement);
1270 $('input[name=doSave]').val(1);
1271 window.open('', 'newTYPO3frontendWindow');
1272 document.editform.submit();
1273 break;
1274 }
1275 };
1276
1277 /**
1278 * Show the preview modal
1279 *
1280 * @param {string} previewUrl
1281 * @param {bool} isNew
1282 * @param {element} $actionElement
1283 * @param {Function} callback
1284 */
1285 FormEngine.showPreviewModal = function(previewUrl, isNew, $actionElement, callback) {
1286 var title = TYPO3.lang['label.confirm.view_record_changed.title'] || 'Do you want to save before viewing?';
1287 var modalCancelButtonConfiguration = {
1288 text: TYPO3.lang['buttons.confirm.view_record_changed.cancel'] || 'Cancel',
1289 btnClass: 'btn-default',
1290 name: 'cancel'
1291 };
1292 var modaldismissViewButtonConfiguration = {
1293 text: TYPO3.lang['buttons.confirm.view_record_changed.no-save'] || 'View without changes',
1294 btnClass: 'btn-info',
1295 name: 'discard'
1296 };
1297 var modalsaveViewButtonConfiguration = {
1298 text: TYPO3.lang['buttons.confirm.view_record_changed.save'] || 'Save changes and view',
1299 btnClass: 'btn-info',
1300 name: 'save',
1301 active: true
1302 };
1303 var modalButtons = [];
1304 var content = '';
1305 if (isNew) {
1306 modalButtons = [
1307 modalCancelButtonConfiguration,
1308 modalsaveViewButtonConfiguration
1309 ];
1310 content = (
1311 TYPO3.lang['label.confirm.view_record_changed.content.is-new-page']
1312 || 'You need to save your changes before viewing the page. Do you want to save and view them now?'
1313 );
1314 } else {
1315 modalButtons = [
1316 modalCancelButtonConfiguration,
1317 modaldismissViewButtonConfiguration,
1318 modalsaveViewButtonConfiguration
1319 ];
1320 content = (
1321 TYPO3.lang['label.confirm.view_record_changed.content']
1322 || 'You currently have unsaved changes. You can either discard these changes or save and view them.'
1323 )
1324 }
1325 var $modal = Modal.confirm(title, content, Severity.info, modalButtons);
1326 $modal.on('button.clicked', function (event) {
1327 callback(event.target.name, previewUrl, $actionElement, $modal);
1328 });
1329 };
1330
1331 /**
1332 * New action
1333 *
1334 * When there are changes:
1335 * Will take action based on local storage preset
1336 * If preset is not available, a modal will open
1337 *
1338 * @param {Event} event
1339 * @param {Function} callback
1340 */
1341 FormEngine.newAction = function(event, callback) {
1342 callback = callback || FormEngine.newActionCallback;
1343
1344 var $actionElement = $('<input />').attr('type', 'hidden').attr('name', '_savedoknew').attr('value', '1');
1345 var isNew = event.target.dataset.hasOwnProperty('isNew');
1346 if (FormEngine.hasChange()) {
1347 FormEngine.showNewModal(isNew, $actionElement, callback);
1348 } else {
1349 $('form[name=' + FormEngine.formName + ']').append($actionElement);
1350 document.editform.submit();
1351 }
1352 };
1353
1354 /**
1355 * The callback for the preview action
1356 *
1357 * @param {string} modalButtonName
1358 * @param {element} $actionElement
1359 */
1360 FormEngine.newActionCallback = function(modalButtonName, $actionElement) {
1361 var $form = $('form[name=' + FormEngine.formName + ']');
1362 Modal.dismiss();
1363 switch(modalButtonName) {
1364 case 'no':
1365 $form.append($actionElement);
1366 document.editform.submit();
1367 break;
1368 case 'yes':
1369 $form.append($actionElement);
1370 $('input[name=doSave]').val(1);
1371 document.editform.submit();
1372 break;
1373 }
1374 };
1375
1376 /**
1377 * Show the new modal
1378 *
1379 * @param {element} $actionElement
1380 * @param {Function} callback
1381 * @param {bool} isNew
1382 */
1383 FormEngine.showNewModal = function(isNew, $actionElement, callback) {
1384 var title = TYPO3.lang['label.confirm.new_record_changed.title'] || 'Do you want to save before adding?';
1385 var content = (
1386 TYPO3.lang['label.confirm.new_record_changed.content']
1387 || 'You need to save your changes before creating a new record. Do you want to save and create now?'
1388 );
1389 var modalButtons = [];
1390 var modalCancelButtonConfiguration = {
1391 text: TYPO3.lang['buttons.confirm.new_record_changed.cancel'] || 'Cancel',
1392 btnClass: 'btn-default',
1393 name: 'cancel'
1394 };
1395 var modalNoButtonConfiguration = {
1396 text: TYPO3.lang['buttons.confirm.new_record_changed.no'] || 'No, just add',
1397 btnClass: 'btn-default',
1398 name: 'no'
1399 };
1400 var modalYesButtonConfiguration = {
1401 text: TYPO3.lang['buttons.confirm.new_record_changed.yes'] || 'Yes, save and create now',
1402 btnClass: 'btn-info',
1403 name: 'yes',
1404 active: true
1405 };
1406 if (isNew) {
1407 modalButtons = [
1408 modalCancelButtonConfiguration,
1409 modalYesButtonConfiguration
1410 ];
1411 } else {
1412 modalButtons = [
1413 modalCancelButtonConfiguration,
1414 modalNoButtonConfiguration,
1415 modalYesButtonConfiguration
1416 ];
1417 }
1418 var $modal = Modal.confirm(title, content, Severity.info, modalButtons);
1419 $modal.on('button.clicked', function (event) {
1420 callback(event.target.name, $actionElement);
1421 });
1422 };
1423
1424 /**
1425 * Duplicate action
1426 *
1427 * When there are changes:
1428 * Will take action based on local storage preset
1429 * If preset is not available, a modal will open
1430 *
1431 * @param {Event} event
1432 * @param {Function} callback
1433 */
1434 FormEngine.duplicateAction = function(event, callback) {
1435 callback = callback || FormEngine.duplicateActionCallback;
1436
1437 var $actionElement = $('<input />').attr('type', 'hidden').attr('name', '_duplicatedoc').attr('value', '1');
1438 var isNew = event.target.dataset.hasOwnProperty('isNew');
1439 if (FormEngine.hasChange()) {
1440 FormEngine.showDuplicateModal(isNew, $actionElement, callback);
1441 } else {
1442 $('form[name=' + FormEngine.formName + ']').append($actionElement);
1443 document.editform.submit();
1444 }
1445 };
1446
1447 /**
1448 * The callback for the duplicate action
1449 *
1450 * @param {string} modalButtonName
1451 * @param {element} $actionElement
1452 */
1453 FormEngine.duplicateActionCallback = function(modalButtonName, $actionElement) {
1454 var $form = $('form[name=' + FormEngine.formName + ']');
1455 Modal.dismiss();
1456 switch(modalButtonName) {
1457 case 'no':
1458 $form.append($actionElement);
1459 document.editform.submit();
1460 break;
1461 case 'yes':
1462 $form.append($actionElement);
1463 $('input[name=doSave]').val(1);
1464 document.editform.submit();
1465 break;
1466 }
1467 };
1468
1469 /**
1470 * Show the duplicate modal
1471 *
1472 * @param {bool} isNew
1473 * @param {element} $actionElement
1474 * @param {Function} callback
1475 */
1476 FormEngine.showDuplicateModal = function(isNew, $actionElement, callback) {
1477 var title = TYPO3.lang['label.confirm.duplicate_record_changed.title'] || 'Do you want to save before duplicating this record?';
1478 var content = (
1479 TYPO3.lang['label.confirm.duplicate_record_changed.content']
1480 || 'You currently have unsaved changes. Do you want to save your changes before duplicating this record?'
1481 );
1482 var modalButtons = [];
1483 var modalCancelButtonConfiguration = {
1484 text: TYPO3.lang['buttons.confirm.duplicate_record_changed.cancel'] || 'Cancel',
1485 btnClass: 'btn-default',
1486 name: 'cancel'
1487 };
1488 var modalDismissDuplicateButtonConfiguration = {
1489 text: TYPO3.lang['buttons.confirm.duplicate_record_changed.no'] || 'No, just duplicate the original',
1490 btnClass: 'btn-default',
1491 name: 'no'
1492 };
1493 var modalSaveDuplicateButtonConfiguration = {
1494 text: TYPO3.lang['buttons.confirm.duplicate_record_changed.yes'] || 'Yes, save and duplicate this record',
1495 btnClass: 'btn-info',
1496 name: 'yes',
1497 active: true
1498 };
1499 if (isNew) {
1500 modalButtons = [
1501 modalCancelButtonConfiguration,
1502 modalSaveDuplicateButtonConfiguration
1503 ];
1504 } else {
1505 modalButtons = [
1506 modalCancelButtonConfiguration,
1507 modalDismissDuplicateButtonConfiguration,
1508 modalSaveDuplicateButtonConfiguration
1509 ];
1510 }
1511 var $modal = Modal.confirm(title, content, Severity.info, modalButtons);
1512 $modal.on('button.clicked', function (event) {
1513 callback(event.target.name, $actionElement);
1514 });
1515 };
1516
1517 /**
1518 * Delete action
1519 *
1520 * When there are changes:
1521 * Will take action based on local storage preset
1522 * If preset is not available, a modal will open
1523 *
1524 * @param {Event} event
1525 * @param {Function} callback
1526 */
1527 FormEngine.deleteAction = function(event, callback) {
1528 callback = callback || FormEngine.deleteActionCallback;
1529
1530 var $anchorElement = $(event.target);
1531
1532 FormEngine.showDeleteModal($anchorElement, callback);
1533 };
1534
1535 /**
1536 * The callback for the delete action
1537 *
1538 * @param {string} modalButtonName
1539 * @param {element} $anchorElement
1540 */
1541 FormEngine.deleteActionCallback = function(modalButtonName, $anchorElement) {
1542 Modal.dismiss();
1543 switch(modalButtonName) {
1544 case 'yes':
1545 deleteRecord($anchorElement.data('table'), $anchorElement.data('uid'), $anchorElement.data('return-url'));
1546 break;
1547 }
1548 };
1549
1550 /**
1551 * Show the delete modal
1552 *
1553 * @param {element} $anchorElement
1554 * @param {Function} callback
1555 */
1556 FormEngine.showDeleteModal = function($anchorElement, callback) {
1557 var title = TYPO3.lang['label.confirm.delete_record.title'] || 'Delete this record?';
1558 var content = TYPO3.lang['label.confirm.delete_record.content'] || 'Are you sure you want to delete this record?';
1559
1560 if ($anchorElement.data('reference-count-message')) {
1561 content += ' ' + $anchorElement.data('reference-count-message');
1562 }
1563
1564 if ($anchorElement.data('translation-count-message')) {
1565 content += ' ' + $anchorElement.data('translation-count-message');
1566 }
1567
1568 var $modal = Modal.confirm(title, content, Severity.warning, [
1569 {
1570 text: TYPO3.lang['buttons.confirm.delete_record.no'] || 'Cancel',
1571 btnClass: 'btn-default',
1572 name: 'no'
1573 },
1574 {
1575 text: TYPO3.lang['buttons.confirm.delete_record.yes'] || 'Yes, delete this record',
1576 btnClass: 'btn-warning',
1577 name: 'yes',
1578 active: true
1579 }
1580 ]);
1581 $modal.on('button.clicked', function (event) {
1582 callback(event.target.name, $anchorElement);
1583 });
1584 };
1585
1586 /**
1587 * Close current open document
1588 */
1589 FormEngine.closeDocument = function() {
1590 document.editform.closeDoc.value = 1;
1591 document.editform.submit();
1592 };
1593
1594 /**
1595 * Main init function called from outside
1596 *
1597 * Sets some options and registers the DOMready handler to initialize further things
1598 *
1599 * @param {String} browserUrl
1600 * @param {Number} mode
1601 */
1602 FormEngine.initialize = function(browserUrl, mode) {
1603 FormEngine.browserUrl = browserUrl;
1604 FormEngine.Validation.setUsMode(mode);
1605
1606 $(function() {
1607 FormEngine.initializeEvents();
1608 FormEngine.SelectBoxFilter.initializeEvents();
1609 FormEngine.initializeSelectCheckboxes();
1610 FormEngine.Validation.initialize();
1611 FormEngine.reinitialize();
1612 $('#t3js-ui-block').remove();
1613 });
1614 };
1615
1616 // load required modules to hook in the post initialize function
1617 if (undefined !== TYPO3.settings.RequireJS && undefined !== TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/FormEngine']) {
1618 $.each(TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/FormEngine'], function(pos, moduleName) {
1619 require([moduleName]);
1620 });
1621 }
1622
1623 // make the form engine object publicly visible for other objects in the TYPO3 namespace
1624 TYPO3.FormEngine = FormEngine;
1625
1626 // return the object in the global space
1627 return FormEngine;
1628 });