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