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