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