0ac3f1f5111a08c934f5ba5b8e3fa53d3be84fb7
[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 $fieldEl.empty();
137 }
138
139 // Clear elements if exclusive values are found
140 if (exclusiveValues) {
141 var reenableOptions = false;
142
143 var m = new RegExp('(^|,)' + value + '($|,)');
144 // the new value is exclusive => remove all existing values
145 if (exclusiveValues.match(m)) {
146 $fieldEl.empty();
147 reenableOptions = true;
148 } else if ($fieldEl.find('option').length == 1) {
149 // there is an old value and it was exclusive => it has to be removed
150 m = new RegExp("(^|,)" + $fieldEl.find('option').prop('value') + "($|,)");
151 if (exclusiveValues.match(m)) {
152 $fieldEl.empty();
153 reenableOptions = true;
154 }
155 }
156
157 if (reenableOptions && typeof $optionEl !== 'undefined') {
158 $optionEl.closest('select').find('[disabled]').removeClass('hidden').prop('disabled', false)
159 }
160 }
161
162 // Inserting the new element
163 var addNewValue = true;
164
165 // check if there is a "_mul" field (a field on the right) and if the field was already added
166 var $multipleFieldEl = FormEngine.getFieldElement(fieldName, '_mul', true);
167 if ($multipleFieldEl.length == 0 || $multipleFieldEl.val() == 0) {
168 $fieldEl.find('option').each(function(k, optionEl) {
169 if ($(optionEl).prop('value') == value) {
170 addNewValue = false;
171 return false;
172 }
173 });
174
175 if (addNewValue && typeof $optionEl !== 'undefined') {
176 $optionEl.addClass('hidden').prop('disabled', true);
177 }
178 }
179
180 // element can be added
181 if (addNewValue) {
182 // finally add the option
183 var $option = $('<option></option>');
184 $option.attr({value: value, title: title}).text(label);
185 $option.appendTo($fieldEl);
186
187 // set the hidden field
188 FormEngine.updateHiddenFieldValueFromSelect($fieldEl, $originalFieldEl);
189
190 // execute the phpcode from $FormEngine->TBE_EDITOR_fieldChanged_func
191 FormEngine.legacyFieldChangedCb();
192 }
193
194 } else {
195
196 // The incoming value consists of the table name, an underscore and the uid
197 // or just the uid
198 // For a single selection field we need only the uid, so we extract it
199 var pattern = /_(\\d+)$/
200 , result = value.toString().match(pattern);
201
202 if (result != null) {
203 value = result[1];
204 }
205
206 // Change the selected value
207 $fieldEl.val(value);
208 }
209 if (typeof FormEngine.Validation !== 'undefined' && typeof FormEngine.Validation.validate === 'function') {
210 FormEngine.Validation.validate();
211 }
212 };
213
214 /**
215 * sets the value of the hidden field, from the select list, always executed after the select field was updated
216 * previously known as global function setHiddenFromList()
217 *
218 * @param {HTMLElement} selectFieldEl the select field
219 * @param {HTMLElement} originalFieldEl the hidden form field
220 */
221 FormEngine.updateHiddenFieldValueFromSelect = setHiddenFromList = function(selectFieldEl, originalFieldEl) {
222 var selectedValues = [];
223 $(selectFieldEl).find('option').each(function() {
224 selectedValues.push($(this).prop('value'));
225 });
226
227 // make a comma separated list, if it is a multi-select
228 // set the values to the final hidden field
229 $(originalFieldEl).val(selectedValues.join(','));
230 };
231
232 /**
233 * legacy function, can be removed once this function is not in use anymore
234 *
235 * @param {String} fName
236 * @param {String} type
237 * @param {Number} maxLength
238 */
239 setFormValueManipulate = function(fName, type, maxLength) {
240 var $formEl = FormEngine.getFormElement(fName);
241 if ($formEl.length > 0) {
242 var formObj = $formEl.get(0);
243 var localArray_V = [];
244 var localArray_L = [];
245 var localArray_S = [];
246 var localArray_T = [];
247 var fObjSel = formObj[fName + '_list'];
248 var l = fObjSel.length;
249 var c = 0;
250 var a;
251
252 if (type === 'RemoveFirstIfFull') {
253 if (maxLength == 1) {
254 for (a = 1; a < l; a++) {
255 if (fObjSel.options[a].selected != 1) {
256 localArray_V[c] = fObjSel.options[a].value;
257 localArray_L[c] = fObjSel.options[a].text;
258 localArray_S[c] = 0;
259 localArray_T[c] = fObjSel.options[a].title;
260 c++;
261 }
262 }
263 } else {
264 return;
265 }
266 }
267
268 if ((type === "Remove" && fObjSel.size > 1) || type === "Top" || type === "Bottom") {
269 if (type === "Top") {
270 for (a = 0; a < l; a++) {
271 if (fObjSel.options[a].selected == 1) {
272 localArray_V[c] = fObjSel.options[a].value;
273 localArray_L[c] = fObjSel.options[a].text;
274 localArray_S[c] = 1;
275 localArray_T[c] = fObjSel.options[a].title;
276 c++;
277 }
278 }
279 }
280 for (a = 0; a < l; a++) {
281 if (fObjSel.options[a].selected != 1) {
282 localArray_V[c] = fObjSel.options[a].value;
283 localArray_L[c] = fObjSel.options[a].text;
284 localArray_S[c] = 0;
285 localArray_T[c] = fObjSel.options[a].title;
286 c++;
287 }
288 }
289 if (type === "Bottom") {
290 for (a = 0; a < l; a++) {
291 if (fObjSel.options[a].selected == 1) {
292 localArray_V[c] = fObjSel.options[a].value;
293 localArray_L[c] = fObjSel.options[a].text;
294 localArray_S[c] = 1;
295 localArray_T[c] = fObjSel.options[a].title;
296 c++;
297 }
298 }
299 }
300 }
301 if (type === "Down") {
302 var tC = 0;
303 var tA = [];
304 var aa = 0;
305
306 for (a = 0; a < l; a++) {
307 if (fObjSel.options[a].selected != 1) {
308 // Add non-selected element:
309 localArray_V[c] = fObjSel.options[a].value;
310 localArray_L[c] = fObjSel.options[a].text;
311 localArray_S[c] = 0;
312 localArray_T[c] = fObjSel.options[a].title;
313 c++;
314
315 // Transfer any accumulated and reset:
316 if (tA.length > 0) {
317 for (aa = 0; aa < tA.length; aa++) {
318 localArray_V[c] = fObjSel.options[tA[aa]].value;
319 localArray_L[c] = fObjSel.options[tA[aa]].text;
320 localArray_S[c] = 1;
321 localArray_T[c] = fObjSel.options[tA[aa]].title;
322 c++;
323 }
324
325 tC = 0;
326 tA = [];
327 }
328 } else {
329 tA[tC] = a;
330 tC++;
331 }
332 }
333 // Transfer any remaining:
334 if (tA.length > 0) {
335 for (aa = 0; aa < tA.length; aa++) {
336 localArray_V[c] = fObjSel.options[tA[aa]].value;
337 localArray_L[c] = fObjSel.options[tA[aa]].text;
338 localArray_S[c] = 1;
339 localArray_T[c] = fObjSel.options[tA[aa]].title;
340 c++;
341 }
342 }
343 }
344 if (type === "Up") {
345 var tC = 0;
346 var tA = [];
347 var aa = 0;
348 c = l - 1;
349
350 for (a = l - 1; a >= 0; a--) {
351 if (fObjSel.options[a].selected != 1) {
352
353 // Add non-selected element:
354 localArray_V[c] = fObjSel.options[a].value;
355 localArray_L[c] = fObjSel.options[a].text;
356 localArray_S[c] = 0;
357 localArray_T[c] = fObjSel.options[a].title;
358 c--;
359
360 // Transfer any accumulated and reset:
361 if (tA.length > 0) {
362 for (aa = 0; aa < tA.length; aa++) {
363 localArray_V[c] = fObjSel.options[tA[aa]].value;
364 localArray_L[c] = fObjSel.options[tA[aa]].text;
365 localArray_S[c] = 1;
366 localArray_T[c] = fObjSel.options[tA[aa]].title;
367 c--;
368 }
369
370 tC = 0;
371 tA = [];
372 }
373 } else {
374 tA[tC] = a;
375 tC++;
376 }
377 }
378 // Transfer any remaining:
379 if (tA.length > 0) {
380 for (aa = 0; aa < tA.length; aa++) {
381 localArray_V[c] = fObjSel.options[tA[aa]].value;
382 localArray_L[c] = fObjSel.options[tA[aa]].text;
383 localArray_S[c] = 1;
384 localArray_T[c] = fObjSel.options[tA[aa]].title;
385 c--;
386 }
387 }
388 c = l; // Restore length value in "c"
389 }
390
391 // Transfer items in temporary storage to list object:
392 fObjSel.length = c;
393 for (a = 0; a < c; a++) {
394 fObjSel.options[a].value = localArray_V[a];
395 fObjSel.options[a].text = localArray_L[a];
396 fObjSel.options[a].selected = localArray_S[a];
397 fObjSel.options[a].title = localArray_T[a];
398 }
399 FormEngine.updateHiddenFieldValueFromSelect(fObjSel, formObj[fName]);
400
401 FormEngine.legacyFieldChangedCb();
402 }
403 };
404
405
406 /**
407 * Legacy function
408 * returns the DOM object for the given form name of the current form,
409 * but only if the given field name is valid, legacy function, use "getFormElement" instead
410 *
411 * @param {String} fieldName the name of the field name
412 * @returns {*|HTMLElement}
413 */
414 setFormValue_getFObj = function(fieldName) {
415 var $formEl = FormEngine.getFormElement(fieldName);
416 if ($formEl.length > 0) {
417 // return the DOM element of the form object
418 return $formEl.get(0);
419 }
420 return null;
421 };
422
423 /**
424 * returns a jQuery object for the given form name of the current form,
425 * if the parameter "fieldName" is given, then the form element is only returned if the field name is available
426 * the latter behaviour mirrors the one of the function "setFormValue_getFObj"
427 *
428 * @param {String} fieldName the field name to check for, optional
429 * @returns {*|HTMLElement}
430 */
431 FormEngine.getFormElement = function(fieldName) {
432 var $formEl = $('form[name="' + FormEngine.formName + '"]:first');
433 if (fieldName) {
434 var $fieldEl = FormEngine.getFieldElement(fieldName)
435 , $listFieldEl = FormEngine.getFieldElement(fieldName, '_list');
436
437 // Take the form object if it is either of type select-one or of type-multiple and it has a "_list" element
438 if ($fieldEl.length > 0 &&
439 (
440 ($fieldEl.prop('type') === 'select-one') ||
441 ($listFieldEl.length > 0 && $listFieldEl.prop('type').match(/select-(one|multiple)/))
442 )
443 ) {
444 return $formEl;
445 } else {
446 console.error('Form fields missing: form: ' + FormEngine.formName + ', field name: ' + fieldName);
447 alert('Form field is invalid');
448 }
449 } else {
450 return $formEl;
451 }
452 };
453
454
455 /**
456 * Returns a jQuery object of the field DOM element of the current form, can also be used to
457 * request an alternative field like "_hr", "_list" or "_mul"
458 *
459 * @param {String} fieldName the name of the field (<input name="fieldName">)
460 * @param {String} appendix optional
461 * @param {Boolean} noFallback if set, then the appendix value is returned no matter if it exists or not
462 * @returns {*|HTMLElement}
463 */
464 FormEngine.getFieldElement = function(fieldName, appendix, noFallback) {
465 var $formEl = FormEngine.getFormElement();
466
467 // if an appendix is set, return the field with the appendix (like _mul or _list)
468 if (appendix) {
469 var $fieldEl;
470 switch (appendix) {
471 case '_list':
472 $fieldEl = $(':input.tceforms-multiselect[data-formengine-input-name="' + fieldName + '"]', $formEl);
473 break;
474 case '_avail':
475 $fieldEl = $(':input[data-relatedfieldname="' + fieldName + '"]', $formEl);
476 break;
477 case '_mul':
478 case '_hr':
479 $fieldEl = $(':input[type=hidden][data-formengine-input-name="' + fieldName + '"]', $formEl);
480 break;
481 }
482 if (($fieldEl && $fieldEl.length > 0) || noFallback === true) {
483 return $fieldEl;
484 }
485 }
486
487 return $(':input[name="' + fieldName + '"]', $formEl);
488 };
489
490
491 /**************************************************
492 * manipulate existing options in a select field
493 **************************************************/
494
495 /**
496 * Moves currently selected options from a select field to the very top,
497 * can be multiple entries as well
498 *
499 * @param {Object} $fieldEl a jQuery object, containing the select field
500 */
501 FormEngine.moveOptionToTop = function($fieldEl) {
502 // remove the selected options
503 var selectedOptions = $fieldEl.find(':selected').detach();
504 // and add them on first position again
505 $fieldEl.prepend(selectedOptions);
506 };
507
508
509 /**
510 * moves currently selected options from a select field up by one position,
511 * can be multiple entries as well
512 *
513 * @param {Object} $fieldEl a jQuery object, containing the select field
514 */
515 FormEngine.moveOptionUp = function($fieldEl) {
516 // remove the selected options and add it before the previous sibling
517 $.each($fieldEl.find(':selected'), function(k, optionEl) {
518 var $optionEl = $(optionEl)
519 , $optionBefore = $optionEl.prev();
520
521 // stop if first option to move is already the first one
522 if (k == 0 && $optionBefore.length === 0) {
523 return false;
524 }
525
526 $optionBefore.before($optionEl.detach());
527 });
528 };
529
530
531 /**
532 * moves currently selected options from a select field down one position,
533 * can be multiple entries as well
534 *
535 * @param {Object} $fieldEl a jQuery object, containing the select field
536 */
537 FormEngine.moveOptionDown = function($fieldEl) {
538 // remove the selected options and add it after the next sibling
539 // however, this time, we need to go from the last to the first
540 var selectedOptions = $fieldEl.find(':selected');
541 selectedOptions = $.makeArray(selectedOptions);
542 selectedOptions.reverse();
543 $.each(selectedOptions, function(k, optionEl) {
544 var $optionEl = $(optionEl)
545 , $optionAfter = $optionEl.next();
546
547 // stop if first option to move is already the last one
548 if (k == 0 && $optionAfter.length === 0) {
549 return false;
550 }
551
552 $optionAfter.after($optionEl.detach());
553 });
554 };
555
556
557 /**
558 * moves currently selected options from a select field as the very last entries
559 *
560 * @param {Object} $fieldEl a jQuery object, containing the select field
561 */
562 FormEngine.moveOptionToBottom = function($fieldEl) {
563 // remove the selected options
564 var selectedOptions = $fieldEl.find(':selected').detach();
565 // and add them on last position again
566 $fieldEl.append(selectedOptions);
567 };
568
569 /**
570 * removes currently selected options from a select field
571 *
572 * @param {Object} $fieldEl a jQuery object, containing the select field
573 * @param {Object} $availableFieldEl a jQuery object, containing all available value
574 */
575 FormEngine.removeOption = function($fieldEl, $availableFieldEl) {
576 var $selected = $fieldEl.find(':selected');
577
578 $selected.each(function() {
579 $availableFieldEl
580 .find('option[value="' + $.escapeSelector($(this).attr('value')) + '"]')
581 .removeClass('hidden')
582 .prop('disabled', false);
583 });
584
585 // remove the selected options
586 $selected.remove();
587 };
588
589
590 /**
591 * Initialize events for all form engine relevant tasks.
592 * This function only needs to be called once on page load,
593 * as it using deferrer methods only
594 */
595 FormEngine.initializeEvents = function() {
596 if (top.TYPO3 && typeof top.TYPO3.Backend !== 'undefined') {
597 top.TYPO3.Backend.consumerScope.attach(FormEngine);
598 $(window).on('unload', function() {
599 top.TYPO3.Backend.consumerScope.detach(FormEngine);
600 });
601 }
602 $(document).on('click', '.t3js-btn-moveoption-top, .t3js-btn-moveoption-up, .t3js-btn-moveoption-down, .t3js-btn-moveoption-bottom, .t3js-btn-removeoption', function(evt) {
603 evt.preventDefault();
604
605 // track the arrows "Up", "Down", "Clear" etc in multi-select boxes
606 var $el = $(this)
607 , fieldName = $el.data('fieldname')
608 , $listFieldEl = FormEngine.getFieldElement(fieldName, '_list');
609
610 if ($listFieldEl.length > 0) {
611
612 if ($el.hasClass('t3js-btn-moveoption-top')) {
613 FormEngine.moveOptionToTop($listFieldEl);
614 } else if ($el.hasClass('t3js-btn-moveoption-up')) {
615 FormEngine.moveOptionUp($listFieldEl);
616 } else if ($el.hasClass('t3js-btn-moveoption-down')) {
617 FormEngine.moveOptionDown($listFieldEl);
618 } else if ($el.hasClass('t3js-btn-moveoption-bottom')) {
619 FormEngine.moveOptionToBottom($listFieldEl);
620 } else if ($el.hasClass('t3js-btn-removeoption')) {
621 var $availableFieldEl = FormEngine.getFieldElement(fieldName, '_avail');
622 FormEngine.removeOption($listFieldEl, $availableFieldEl);
623 }
624
625 // make sure to update the hidden field value when modifying the select value
626 FormEngine.updateHiddenFieldValueFromSelect($listFieldEl, FormEngine.getFieldElement(fieldName));
627 FormEngine.legacyFieldChangedCb();
628 if (typeof FormEngine.Validation !== 'undefined' && typeof FormEngine.Validation.validate === 'function') {
629 FormEngine.Validation.validate();
630 }
631 }
632 }).on('click', '.t3js-formengine-select-itemstoselect', function(evt) {
633 // in multi-select environments with two (e.g. "Access"), on click the item from the right should go to the left
634 var $el = $(this)
635 , fieldName = $el.data('relatedfieldname')
636 , exclusiveValues = $el.data('exclusivevalues');
637
638 if (fieldName) {
639 // try to add each selected field to the "left" select field
640 $el.find(':selected').each(function() {
641 var $optionEl = $(this);
642 FormEngine.setSelectOptionFromExternalSource(fieldName, $optionEl.prop('value'), $optionEl.text(), $optionEl.prop('title'), exclusiveValues, $optionEl);
643 });
644 }
645 }).on('click', '.t3js-editform-close', function(e) {
646 e.preventDefault();
647 FormEngine.preventExitIfNotSaved(
648 FormEngine.preventExitIfNotSavedCallback
649 );
650 }).on('click', '.t3js-editform-duplicate', function(e) {
651 e.preventDefault();
652 var $elem = $('<input />').attr('type', 'hidden').attr('name', '_duplicatedoc').attr('value', '1');
653 if ($('form[name="' + FormEngine.formName + '"] .has-change').length > 0) {
654 var title = TYPO3.lang['label.confirm.duplicate_record_changed.title'] || 'Duplicate changed record?';
655 var content = TYPO3.lang['label.confirm.duplicate_record_changed.content'] || 'Do you want to save your changes and duplicate the record?';
656 var $modal = Modal.confirm(title, content, Severity.warning, [
657 {
658 text: TYPO3.lang['buttons.confirm.duplicate_record_changed.cancel'] || 'Cancel',
659 active: true,
660 btnClass: 'btn-default',
661 name: 'cancel'
662 },
663 {
664 text: TYPO3.lang['buttons.confirm.duplicate_record_changed.dismiss_and_duplicate'] || 'Dismiss changes and duplicate',
665 active: true,
666 btnClass: 'btn-default',
667 name: 'dismissDuplicate'
668 },
669 {
670 text: TYPO3.lang['buttons.confirm.duplicate_record_changed.save_and_duplicate'] || 'Save changes and duplicate',
671 btnClass: 'btn-success',
672 name: 'saveDuplicate'
673 }
674 ]);
675 $modal.on('button.clicked', function (e) {
676 if (e.target.name === 'cancel') {
677 Modal.dismiss();
678 } else if (e.target.name === 'dismissDuplicate') {
679 $('form[name=' + FormEngine.formName + ']').append($elem);
680 $('input[name=doSave]').val(0);
681 Modal.dismiss();
682 document.editform.submit();
683 } else if (e.target.name == 'saveDuplicate') {
684 $('form[name=' + FormEngine.formName + ']').append($elem);
685 $('input[name=doSave]').val(1);
686 Modal.dismiss();
687 document.editform.submit();
688 }
689 });
690 } else {
691 $('form[name=' + FormEngine.formName + ']').append($elem);
692 document.editform.submit();
693 }
694 }).on('click', '.t3js-editform-delete-record', function(e) {
695 e.preventDefault();
696 var title = TYPO3.lang['label.confirm.delete_record.title'] || 'Delete this record?';
697 var content = TYPO3.lang['label.confirm.delete_record.content'] || 'Are you sure you want to delete this record?';
698 var $anchorElement = $(this);
699 var $modal = Modal.confirm(title, content, Severity.warning, [
700 {
701 text: TYPO3.lang['buttons.confirm.delete_record.no'] || 'Cancel',
702 active: true,
703 btnClass: 'btn-default',
704 name: 'no'
705 },
706 {
707 text: TYPO3.lang['buttons.confirm.delete_record.yes'] || 'Yes, delete this record',
708 btnClass: 'btn-warning',
709 name: 'yes'
710 }
711 ]);
712 $modal.on('button.clicked', function(e) {
713 if (e.target.name === 'no') {
714 Modal.dismiss();
715 } else if (e.target.name === 'yes') {
716 deleteRecord($anchorElement.data('table'), $anchorElement.data('uid'), $anchorElement.data('return-url'));
717 Modal.dismiss();
718 }
719 });
720 }).on('click', '.t3js-editform-delete-inline-record', function(e) {
721 e.preventDefault();
722 var title = TYPO3.lang['label.confirm.delete_record.title'] || 'Delete this record?';
723 var content = TYPO3.lang['label.confirm.delete_record.content'] || 'Are you sure you want to delete this record?';
724 var $anchorElement = $(this);
725 var $modal = Modal.confirm(title, content, Severity.warning, [
726 {
727 text: TYPO3.lang['buttons.confirm.delete_record.no'] || 'Cancel',
728 active: true,
729 btnClass: 'btn-default',
730 name: 'no'
731 },
732 {
733 text: TYPO3.lang['buttons.confirm.delete_record.yes'] || 'Yes, delete this record',
734 btnClass: 'btn-warning',
735 name: 'yes'
736 }
737 ]);
738 $modal.on('button.clicked', function(e) {
739 if (e.target.name === 'no') {
740 Modal.dismiss();
741 } else if (e.target.name === 'yes') {
742 var objectId = $anchorElement.data('objectid');
743 inline.deleteRecord(objectId);
744 Modal.dismiss();
745 }
746 });
747 }).on('click', '.t3js-editform-submitButton', function(event) {
748 // remember the clicked submit button. we need to know that in TBE_EDITOR.submitForm();
749 var $me = $(this),
750 name = $me.data('name') || this.name,
751 $elem = $('<input />').attr('type', 'hidden').attr('name', name).attr('value', '1');
752
753 $me.parents('form').append($elem);
754 }).on('change', '.t3-form-field-eval-null-checkbox input[type="checkbox"]', function(e) {
755 // Null checkboxes without placeholder click event handler
756 $(this).closest('.t3js-formengine-field-item').toggleClass('disabled');
757 }).on('change', '.t3js-form-field-eval-null-placeholder-checkbox input[type="checkbox"]', function(e) {
758 $(this).closest('.t3js-formengine-field-item').find('.t3js-formengine-placeholder-placeholder').toggle();
759 $(this).closest('.t3js-formengine-field-item').find('.t3js-formengine-placeholder-formfield').toggle();
760 }).on('change', '.t3js-l10n-state-container input[type=radio]', function(event) {
761 // Change handler for "l10n_state" field changes
762 var $me = $(this);
763 var $input = $me.closest('.t3js-formengine-field-item').find('[data-formengine-input-name]');
764
765 if ($input.length > 0) {
766 var lastState = $input.data('last-l10n-state') || false,
767 currentState = $(this).val();
768
769 if (lastState && currentState === lastState) {
770 return;
771 }
772
773 if (currentState === 'custom') {
774 if (lastState) {
775 $(this).attr('data-original-language-value', $input.val());
776 }
777 $input.attr('disabled', false);
778 } else {
779 if (lastState === 'custom') {
780 $(this).closest('.t3js-l10n-state-container').find('.t3js-l10n-state-custom').attr('data-original-language-value', $input.val());
781 }
782 $input.attr('disabled', 'disabled');
783 }
784
785 $input.val($(this).attr('data-original-language-value')).trigger('change');
786 $input.data('last-l10n-state', $(this).val());
787 }
788 }).on('formengine.dp.change', function(event, $field) {
789 FormEngine.Validation.validate();
790 FormEngine.Validation.markFieldAsChanged($field);
791 });
792 };
793
794 /**
795 * @param {InteractionRequest} interactionRequest
796 * @return {jQuery.Deferred}
797 */
798 FormEngine.consume = function(interactionRequest) {
799 if (!interactionRequest) {
800 throw new BackendException('No interaction request given', 1496589980);
801 }
802 if (interactionRequest.concernsTypes(FormEngine.consumeTypes)) {
803 var outerMostRequest = interactionRequest.outerMostRequest;
804 var deferred = $.Deferred();
805
806 FormEngine.interactionRequestMap.attachFor(
807 outerMostRequest,
808 deferred
809 );
810 // resolve or reject deferreds with previous user choice
811 if (outerMostRequest.isProcessed()) {
812 handleConsumeResponse(
813 outerMostRequest,
814 outerMostRequest.getProcessedData().response
815 );
816 // show confirmation dialog
817 } else if (FormEngine.hasChange()) {
818 FormEngine.preventExitIfNotSaved(function(response) {
819 outerMostRequest.setProcessedData(
820 {response: response}
821 );
822 handleConsumeResponse(outerMostRequest, response);
823 });
824 // resolve directly
825 } else {
826 FormEngine.interactionRequestMap.resolveFor(outerMostRequest);
827 }
828
829 return deferred;
830 }
831 };
832
833 /**
834 * Initializes the remaining character views based on the fields' maxlength attribute
835 */
836 FormEngine.initializeRemainingCharacterViews = function() {
837 // all fields with a "maxlength" attribute
838 var $maxlengthElements = $('[maxlength]').not('.t3js-datetimepicker').not('.t3js-charcounter-initialized');
839 $maxlengthElements.on('focus', function(e) {
840 var $field = $(this),
841 $parent = $field.parents('.t3js-formengine-field-item:first'),
842 maxlengthProperties = FormEngine.getCharacterCounterProperties($field);
843
844 // append the counter only at focus to avoid cluttering the DOM
845 $parent.append($('<div />', {'class': 't3js-charcounter'}).append(
846 $('<span />', {'class': maxlengthProperties.labelClass}).text(TYPO3.lang['FormEngine.remainingCharacters'].replace('{0}', maxlengthProperties.remainingCharacters))
847 ));
848 }).on('blur', function() {
849 var $field = $(this),
850 $parent = $field.parents('.t3js-formengine-field-item:first');
851 $parent.find('.t3js-charcounter').remove();
852 }).on('keyup', function() {
853 var $field = $(this),
854 $parent = $field.parents('.t3js-formengine-field-item:first'),
855 maxlengthProperties = FormEngine.getCharacterCounterProperties($field);
856
857 // change class and value
858 $parent.find('.t3js-charcounter span').removeClass().addClass(maxlengthProperties.labelClass).text(TYPO3.lang['FormEngine.remainingCharacters'].replace('{0}', maxlengthProperties.remainingCharacters))
859 });
860 $maxlengthElements.addClass('t3js-charcounter-initialized');
861 $(':password').on('focus', function() {
862 $(this).attr('type', 'text').select();
863 }).on('blur', function() {
864 $(this).attr('type', 'password');
865 });
866 };
867
868 /**
869 * Initialize select checkbox element checkboxes
870 */
871 FormEngine.initializeSelectCheckboxes = function() {
872 $('.t3js-toggle-checkboxes').each(function() {
873 var $checkbox = $(this);
874 var $table = $checkbox.closest('table');
875 var $checkboxes = $table.find('.t3js-checkbox');
876 var checkIt = $checkboxes.length === $table.find('.t3js-checkbox:checked').length;
877 $checkbox.prop('checked', checkIt);
878 });
879 $(document).on('change', '.t3js-toggle-checkboxes', function(e) {
880 e.preventDefault();
881 var $checkbox = $(this);
882 var $table = $checkbox.closest('table');
883 var $checkboxes = $table.find('.t3js-checkbox');
884 var checkIt = $checkboxes.length !== $table.find('.t3js-checkbox:checked').length;
885 $checkboxes.prop('checked', checkIt);
886 $checkbox.prop('checked', checkIt);
887 });
888 $(document).on('change', '.t3js-checkbox', function(e) {
889 FormEngine.updateCheckboxState(this);
890 });
891 };
892
893 /**
894 *
895 * @param {HTMLElement} source
896 */
897 FormEngine.updateCheckboxState = function(source) {
898 var $sourceElement = $(source);
899 var $table = $sourceElement.closest('table');
900 var $checkboxes = $table.find('.t3js-checkbox');
901 var checkIt = $checkboxes.length === $table.find('.t3js-checkbox:checked').length;
902 $table.find('.t3js-toggle-checkboxes').prop('checked', checkIt);
903 };
904
905 /**
906 * Get the properties required for proper rendering of the character counter
907 *
908 * @param {Object} $field
909 * @returns {{remainingCharacters: number, labelClass: string}}
910 */
911 FormEngine.getCharacterCounterProperties = function($field) {
912 var fieldText = $field.val(),
913 maxlength = $field.attr('maxlength'),
914 currentFieldLength = fieldText.length,
915 numberOfLineBreaks = (fieldText.match(/\n/g) || []).length, // count line breaks
916 remainingCharacters = maxlength - currentFieldLength - numberOfLineBreaks,
917 threshold = 15, // hard limit of remaining characters when the label class changes
918 labelClass = '';
919
920 if (remainingCharacters < threshold) {
921 labelClass = 'label-danger';
922 } else if (remainingCharacters < threshold * 2) {
923 labelClass = 'label-warning';
924 } else {
925 labelClass = 'label-info';
926 }
927
928 return {
929 remainingCharacters: remainingCharacters,
930 labelClass: 'label ' + labelClass
931 };
932 };
933
934 /**
935 * Select field filter functions, see TCA option "enableMultiSelectFilterTextfield"
936 * and "multiSelectFilterItems"
937 */
938 FormEngine.SelectBoxFilter = {
939 options: {
940 fieldContainerSelector: '.t3js-formengine-field-group',
941 filterContainerSelector: '.t3js-formengine-multiselect-filter-container',
942 filterTextFieldSelector: '.t3js-formengine-multiselect-filter-textfield',
943 filterSelectFieldSelector: '.t3js-formengine-multiselect-filter-dropdown',
944 itemsToSelectElementSelector: '.t3js-formengine-select-itemstoselect'
945 }
946 };
947
948 /**
949 * Make sure that all selectors and input filters are recognized
950 * note: this also works on elements that are loaded asynchronously via AJAX, no need to call this method
951 * after an AJAX load.
952 */
953 FormEngine.SelectBoxFilter.initializeEvents = function() {
954 $(document).on('keyup', FormEngine.SelectBoxFilter.options.filterTextFieldSelector, function() {
955 var $selectElement = FormEngine.SelectBoxFilter.getSelectElement($(this));
956 FormEngine.SelectBoxFilter.filter($selectElement, $(this).val());
957 }).on('change', FormEngine.SelectBoxFilter.options.filterSelectFieldSelector, function() {
958 var $selectElement = FormEngine.SelectBoxFilter.getSelectElement($(this));
959 FormEngine.SelectBoxFilter.filter($selectElement, $(this).val());
960 });
961 };
962
963 /**
964 * Fetch the "itemstoselect" select element where a filter item is attached to
965 *
966 * @param {Object} $relativeElement
967 * @returns {*}
968 */
969 FormEngine.SelectBoxFilter.getSelectElement = function($relativeElement) {
970 var $containerElement = $relativeElement.closest(FormEngine.SelectBoxFilter.options.fieldContainerSelector);
971 return $containerElement.find(FormEngine.SelectBoxFilter.options.itemsToSelectElementSelector);
972 };
973
974 /**
975 * Filter the actual items
976 *
977 * @param {Object} $selectElement
978 * @param {String} filterText
979 */
980 FormEngine.SelectBoxFilter.filter = function($selectElement, filterText) {
981 var $allOptionElements;
982 if (!$selectElement.data('alloptions')) {
983 $allOptionElements = $selectElement.find('option').clone();
984 $selectElement.data('alloptions', $allOptionElements);
985 } else {
986 $allOptionElements = $selectElement.data('alloptions');
987 }
988
989 if (filterText.length > 0) {
990 var matchFilter = new RegExp(filterText, 'i');
991 $selectElement.html('');
992 $allOptionElements.each(function() {
993 var $item = $(this);
994 if ($item.text().match(matchFilter)) {
995 $selectElement.append($item.clone());
996 }
997 });
998 } else {
999 $selectElement.html($allOptionElements);
1000 }
1001 };
1002
1003 /**
1004 * convert all textareas so they grow when it is typed in.
1005 */
1006 FormEngine.convertTextareasResizable = function() {
1007 var $elements = $('.t3js-formengine-textarea');
1008 if (TYPO3.settings.Textarea && TYPO3.settings.Textarea.autosize && $elements.length) {
1009 require(['autosize'], function(autosize) {
1010 autosize($elements);
1011 });
1012 }
1013 };
1014
1015 /**
1016 * convert all textareas to enable tab
1017 */
1018 FormEngine.convertTextareasEnableTab = function() {
1019 var $elements = $('.t3js-enable-tab');
1020 if ($elements.length) {
1021 require(['taboverride'], function(taboverride) {
1022 taboverride.set($elements);
1023 });
1024 }
1025 };
1026
1027 /**
1028 * Initialize input / text field "null" checkbox CSS overlay if no placeholder is set.
1029 */
1030 FormEngine.initializeNullNoPlaceholderCheckboxes = function() {
1031 $('.t3-form-field-eval-null-checkbox').each(function() {
1032 // Add disabled class to "t3js-formengine-field-item" if the null checkbox is NOT set,
1033 // This activates a CSS overlay "disabling" the input field and everything around.
1034 var $checkbox = $(this).find('input[type="checkbox"]');
1035 var $fieldItem = $(this).closest('.t3js-formengine-field-item');
1036 if (!$checkbox.attr('checked')) {
1037 $fieldItem.addClass('disabled');
1038 }
1039 });
1040 };
1041
1042 /**
1043 * Initialize input / text field "null" checkbox placeholder / real field if placeholder is set.
1044 */
1045 FormEngine.initializeNullWithPlaceholderCheckboxes = function() {
1046 $('.t3js-form-field-eval-null-placeholder-checkbox').each(function() {
1047 // Set initial state of both div's (one containing actual field, other containing placeholder field)
1048 // depending on whether checkbox is checked or not
1049 var $checkbox = $(this).find('input[type="checkbox"]');
1050 if ($checkbox.attr('checked')) {
1051 $(this).closest('.t3js-formengine-field-item').find('.t3js-formengine-placeholder-placeholder').hide();
1052 } else {
1053 $(this).closest('.t3js-formengine-field-item').find('.t3js-formengine-placeholder-formfield').hide();
1054 }
1055 });
1056 };
1057
1058 /**
1059 * This is the main function that is called on page load, but also after elements are asynchronously
1060 * called e.g. after inline elements are loaded, or a new flexform section is added.
1061 * Use this function in your extension like this "TYPO3.FormEngine.initialize()"
1062 * if you add new fields dynamically.
1063 */
1064 FormEngine.reinitialize = function() {
1065 // Apply "close" button to all input / datetime fields
1066 if ($('.t3js-clearable').length) {
1067 require(['TYPO3/CMS/Backend/jquery.clearable'], function() {
1068 $('.t3js-clearable').clearable();
1069 });
1070 }
1071 if ($('.t3-form-suggest').length) {
1072 require(['TYPO3/CMS/Backend/FormEngineSuggest'], function(Suggest) {
1073 Suggest($('.t3-form-suggest'));
1074 });
1075 }
1076 // Apply DatePicker to all date time fields
1077 if ($('.t3js-datetimepicker').length) {
1078 require(['TYPO3/CMS/Backend/DateTimePicker'], function(DateTimePicker) {
1079 DateTimePicker.initialize();
1080 });
1081 }
1082
1083 FormEngine.convertTextareasResizable();
1084 FormEngine.convertTextareasEnableTab();
1085 FormEngine.initializeNullNoPlaceholderCheckboxes();
1086 FormEngine.initializeNullWithPlaceholderCheckboxes();
1087 FormEngine.initializeInputLinkToggle();
1088 FormEngine.initializeLocalizationStateSelector();
1089 FormEngine.initializeRemainingCharacterViews();
1090 };
1091
1092 /**
1093 * Disable the input field on load if localization state selector is set to "parent" or "source"
1094 */
1095 FormEngine.initializeLocalizationStateSelector = function() {
1096 $('.t3js-l10n-state-container').each(function() {
1097 var $input = $(this).closest('.t3js-formengine-field-item').find('[data-formengine-input-name]');
1098 var currentState = $(this).find('input[type="radio"]:checked').val();
1099 if (currentState === 'parent' || currentState === 'source') {
1100 $input.attr('disabled', 'disabled');
1101 }
1102 });
1103 };
1104
1105 /**
1106 * Toggle for input link explanation
1107 */
1108 FormEngine.initializeInputLinkToggle = function() {
1109 var toggleClass = '.t3js-form-field-inputlink-explanation-toggle',
1110 inputFieldClass = '.t3js-form-field-inputlink-input',
1111 explanationClass = '.t3js-form-field-inputlink-explanation';
1112
1113 // if empty, show input field
1114 $(explanationClass).filter(function() {
1115 return !$.trim($(this).val());
1116 }).each(function() {
1117 var $group = $(this).closest('.t3js-form-field-inputlink'),
1118 $inputField = $group.find(inputFieldClass),
1119 $explanationField = $group.find(explanationClass);
1120 $explanationField.toggleClass('hidden', true);
1121 $inputField.toggleClass('hidden', false);
1122 $group.find('.form-control-clearable button.close').toggleClass('hidden', false)
1123 });
1124
1125 $(document).on('click', toggleClass, function(e) {
1126 e.preventDefault();
1127
1128 var $group = $(this).closest('.t3js-form-field-inputlink'),
1129 $inputField = $group.find(inputFieldClass),
1130 $explanationField = $group.find(explanationClass),
1131 explanationShown;
1132
1133 explanationShown = !$explanationField.hasClass('hidden');
1134 $explanationField.toggleClass('hidden', explanationShown);
1135 $inputField.toggleClass('hidden', !explanationShown);
1136 $group.find('.form-control-clearable button.close').toggleClass('hidden', !explanationShown)
1137 });
1138
1139 $(inputFieldClass).on('change', function() {
1140 var $group = $(this).closest('.t3js-form-field-inputlink'),
1141 $inputField = $group.find(inputFieldClass),
1142 $explanationField = $group.find(explanationClass),
1143 explanationShown;
1144
1145 if (!$explanationField.hasClass('hidden')) {
1146
1147 explanationShown = !$explanationField.hasClass('hidden');
1148 $explanationField.toggleClass('hidden', explanationShown);
1149 $inputField.toggleClass('hidden', !explanationShown);
1150 $group.find('.form-control-clearable button.close').toggleClass('hidden', !explanationShown)
1151 }
1152 });
1153 };
1154
1155 /**
1156 * @return {boolean}
1157 */
1158 FormEngine.hasChange = function() {
1159 return $('form[name="' + FormEngine.formName + '"] .has-change').length > 0;
1160 };
1161
1162 /**
1163 * @param {boolean} response
1164 */
1165 FormEngine.preventExitIfNotSavedCallback = function(response) {
1166 if (response) {
1167 FormEngine.closeDocument();
1168 }
1169 };
1170
1171 /**
1172 * Show modal to confirm closing the document without saving.
1173 *
1174 * @param {Function} callback
1175 */
1176 FormEngine.preventExitIfNotSaved = function(callback) {
1177 callback = callback || FormEngine.preventExitIfNotSavedCallback;
1178
1179 if (FormEngine.hasChange()) {
1180 var title = TYPO3.lang['label.confirm.close_without_save.title'] || 'Do you want to quit without saving?';
1181 var content = TYPO3.lang['label.confirm.close_without_save.content'] || 'You have currently unsaved changes. Are you sure that you want to discard all changes?';
1182 var $modal = Modal.confirm(title, content, Severity.warning, [
1183 {
1184 text: TYPO3.lang['buttons.confirm.close_without_save.no'] || 'No, I will continue editing',
1185 active: true,
1186 btnClass: 'btn-default',
1187 name: 'no'
1188 },
1189 {
1190 text: TYPO3.lang['buttons.confirm.close_without_save.yes'] || 'Yes, discard my changes',
1191 btnClass: 'btn-warning',
1192 name: 'yes'
1193 }
1194 ]);
1195 $modal.on('button.clicked', function(e) {
1196 if (e.target.name === 'no') {
1197 Modal.dismiss();
1198 callback.call(null, false);
1199 } else if (e.target.name === 'yes') {
1200 Modal.dismiss();
1201 callback.call(null, true);
1202 }
1203 });
1204 } else {
1205 callback.call(null, true);
1206 }
1207 };
1208
1209 /**
1210 * Show modal to confirm closing the document without saving
1211 */
1212 FormEngine.preventSaveIfHasErrors = function() {
1213 if ($('.has-error').length > 0) {
1214 var title = TYPO3.lang['label.alert.save_with_error.title'] || 'You have errors in your form!';
1215 var content = TYPO3.lang['label.alert.save_with_error.content'] || 'Please check the form, there is at least one error in your form.';
1216 var $modal = Modal.confirm(title, content, Severity.error, [
1217 {
1218 text: TYPO3.lang['buttons.alert.save_with_error.ok'] || 'OK',
1219 btnClass: 'btn-danger',
1220 name: 'ok'
1221 }
1222 ]);
1223 $modal.on('button.clicked', function(e) {
1224 if (e.target.name === 'ok') {
1225 Modal.dismiss();
1226 }
1227 });
1228 return false;
1229 }
1230 return true;
1231 };
1232
1233 /**
1234 * Close current open document
1235 */
1236 FormEngine.closeDocument = function() {
1237 document.editform.closeDoc.value = 1;
1238 document.editform.submit();
1239 };
1240
1241 /**
1242 * Main init function called from outside
1243 *
1244 * Sets some options and registers the DOMready handler to initialize further things
1245 *
1246 * @param {String} browserUrl
1247 * @param {Number} mode
1248 */
1249 FormEngine.initialize = function(browserUrl, mode) {
1250 FormEngine.browserUrl = browserUrl;
1251 FormEngine.Validation.setUsMode(mode);
1252
1253 $(function() {
1254 FormEngine.initializeEvents();
1255 FormEngine.SelectBoxFilter.initializeEvents();
1256 FormEngine.initializeSelectCheckboxes();
1257 FormEngine.Validation.initialize();
1258 FormEngine.reinitialize();
1259 $('#t3js-ui-block').remove();
1260 });
1261 };
1262
1263 // load required modules to hook in the post initialize function
1264 if (undefined !== TYPO3.settings.RequireJS && undefined !== TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/FormEngine']) {
1265 $.each(TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/FormEngine'], function(pos, moduleName) {
1266 require([moduleName]);
1267 });
1268 }
1269
1270 // make the form engine object publicly visible for other objects in the TYPO3 namespace
1271 TYPO3.FormEngine = FormEngine;
1272
1273 // return the object in the global space
1274 return FormEngine;
1275 });