53e24b5184fe5d33c2f492fbfc15663e71ee0479
[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 define('TYPO3/CMS/Backend/FormEngine', ['jquery'], function ($) {
32
33 // main options
34 var FormEngine = {
35 formName: TYPO3.settings.FormEngine.formName
36 ,backPath: TYPO3.settings.FormEngine.backPath
37 ,openedPopupWindow: null
38 ,legacyFieldChangedCb: function() { !$.isFunction(TYPO3.settings.FormEngine.legacyFieldChangedCb) || TYPO3.settings.FormEngine.legacyFieldChangedCb(); }
39 };
40
41
42 // functions to connect the db/file browser with this document and the formfields on it!
43
44 /**
45 * opens a popup window with the element browser (browser.php)
46 *
47 * @param mode can be "db" or "file"
48 * @param params additional params for the browser window
49 */
50 FormEngine.openPopupWindow = setFormValueOpenBrowser = function(mode, params) {
51 var url = FormEngine.backPath + 'browser.php?mode=' + mode + '&bparams=' + params;
52 FormEngine.openedPopupWindow = window.open(url, 'Typo3WinBrowser', 'height=650,width=' + (mode == 'db' ? 650 : 600) + ',status=0,menubar=0,resizable=1,scrollbars=1');
53 FormEngine.openedPopupWindow.focus();
54 };
55
56
57 /**
58 * properly fills the select field from the popup window (element browser, link browser)
59 * or from a multi-select (two selects side-by-side)
60 * previously known as "setFormValueFromBrowseWin"
61 *
62 * @param fieldName formerly known as "fName" name of the field, like [tt_content][2387][header]
63 * @param value the value to fill in (could be an integer)
64 * @param label the visible name in the selector
65 * @param title the title when hovering over it
66 * @param exclusiveValues if the select field has exclusive options that are not combine-able
67 */
68 FormEngine.setSelectOptionFromExternalSource = setFormValueFromBrowseWin = function(fieldName, value, label, title, exclusiveValues) {
69 exclusiveValues = String(exclusiveValues);
70
71 var $originalFieldEl = $fieldEl = FormEngine.getFieldElement(fieldName)
72 ,isMultiple = false
73 ,isList = false;
74
75 if ($originalFieldEl.length == 0 || value === '--div--') {
76 return;
77 }
78
79 // Check if the form object has a "_list" element
80 // The "_list" element exists for multiple selection select types
81 var $listFieldEl = FormEngine.getFieldElement(fieldName, '_list', true);
82 if ($listFieldEl.length > 0) {
83 $fieldEl = $listFieldEl;
84 isMultiple = ($fieldEl.prop('multiple') && $fieldEl.prop('size') != '1');
85 isList = true;
86 }
87
88 // clear field before adding value, if configured so (maxitems==1)
89 // @todo: clean this code
90 if (typeof TBE_EDITOR.clearBeforeSettingFormValueFromBrowseWin[fieldName] != 'undefined') {
91 clearSettings = TBE_EDITOR.clearBeforeSettingFormValueFromBrowseWin[fieldName];
92 $fieldEl.empty();
93
94 // Clear the upload field
95 var filesContainer = document.getElementById(clearSettings.itemFormElID_file);
96 if (filesContainer) {
97 filesContainer.innerHTML = filesContainer.innerHTML;
98 }
99 }
100
101 if (isMultiple || isList) {
102
103 // If multiple values are not allowed, clear anything that is in the control already
104 if (!isMultiple) {
105 $fieldEl.empty();
106 }
107
108 // Clear elements if exclusive values are found
109 if (exclusiveValues) {
110 var m = new RegExp('(^|,)' + value + '($|,)');
111 // the new value is exclusive => remove all existing values
112 if (exclusiveValues.match(m)) {
113 $fieldEl.empty();
114
115 // there is an old value and it was exclusive => it has to be removed
116 } else if ($fieldEl.children('option').length == 1) {
117 m = new RegExp("(^|,)" + $fieldEl.children('option').prop('value') + "($|,)");
118 if (exclusiveValues.match(m)) {
119 $fieldEl.empty();
120 }
121 }
122 }
123
124 // Inserting the new element
125 var addNewValue = true;
126
127 // check if there is a "_mul" field (a field on the right) and if the field was already added
128 var $multipleFieldEl = FormEngine.getFieldElement(fieldName, '_mul', true);
129 if ($multipleFieldEl.length == 0 || $multipleFieldEl.val() == 0) {
130 $fieldEl.children('option').each(function(k, optionEl) {
131 if ($(optionEl).prop('value') == value) {
132 addNewValue = false;
133 return false;
134 }
135 });
136 }
137
138 // element can be added
139 if (addNewValue) {
140 // finally add the option
141 var $option = $('<option value="' + value + '" title="' + title + '"></option>');
142 $option.attr({value: value, title: title}).text(label);
143 $option.appendTo($fieldEl);
144
145 // set the hidden field
146 FormEngine.updateHiddenFieldValueFromSelect($fieldEl, $originalFieldEl);
147
148 // execute the phpcode from $FormEngine->TBE_EDITOR_fieldChanged_func
149 FormEngine.legacyFieldChangedCb();
150 }
151
152 } else {
153
154 // The incoming value consists of the table name, an underscore and the uid
155 // For a single selection field we need only the uid, so we extract it
156 var pattern = /_(\\d+)$/
157 ,result = value.match(pattern);
158
159 if (result != null) {
160 value = result[1];
161 }
162
163 // Change the selected value
164 $fieldEl.val(value);
165 }
166 };
167
168 /**
169 * sets the value of the hidden field, from the select list, always executed after the select field was updated
170 * previously known as global function setHiddenFromList()
171 *
172 * @param selectFieldEl the select field
173 * @param originalFieldEl the hidden form field
174 */
175 FormEngine.updateHiddenFieldValueFromSelect = setHiddenFromList = function(selectFieldEl, originalFieldEl) {
176 var selectedValues = [];
177 $(selectFieldEl).children('option').each(function() {
178 selectedValues.push($(this).prop('value'));
179 });
180
181 // make a comma separated list, if it is a multi-select
182 // set the values to the final hidden field
183 $(originalFieldEl).val(selectedValues.join(','));
184 };
185
186 // legacy function, can be removed once this function is not in use anymore
187 setFormValueManipulate = function(fName, type, maxLength) {
188 var $formEl = FormEngine.getFormElement(fName);
189 if ($formEl.length > 0) {
190 var formObj = $formEl.get(0);
191 var localArray_V = new Array();
192 var localArray_L = new Array();
193 var localArray_S = new Array();
194 var localArray_T = new Array();
195 var fObjSel = formObj[fName + '_list'];
196 var l = fObjSel.length;
197 var c = 0;
198
199 if (type == 'RemoveFirstIfFull') {
200 if (maxLength == 1) {
201 for (a = 1; a < l; a++) {
202 if (fObjSel.options[a].selected != 1) {
203 localArray_V[c] = fObjSel.options[a].value;
204 localArray_L[c] = fObjSel.options[a].text;
205 localArray_S[c] = 0;
206 localArray_T[c] = fObjSel.options[a].title;
207 c++;
208 }
209 }
210 } else {
211 return;
212 }
213 }
214
215 if ((type=="Remove" && fObjSel.size > 1) || type=="Top" || type=="Bottom") {
216 if (type=="Top") {
217 for (a=0;a<l;a++) {
218 if (fObjSel.options[a].selected==1) {
219 localArray_V[c]=fObjSel.options[a].value;
220 localArray_L[c]=fObjSel.options[a].text;
221 localArray_S[c]=1;
222 localArray_T[c] = fObjSel.options[a].title;
223 c++;
224 }
225 }
226 }
227 for (a=0;a<l;a++) {
228 if (fObjSel.options[a].selected!=1) {
229 localArray_V[c]=fObjSel.options[a].value;
230 localArray_L[c]=fObjSel.options[a].text;
231 localArray_S[c]=0;
232 localArray_T[c] = fObjSel.options[a].title;
233 c++;
234 }
235 }
236 if (type=="Bottom") {
237 for (a=0;a<l;a++) {
238 if (fObjSel.options[a].selected==1) {
239 localArray_V[c]=fObjSel.options[a].value;
240 localArray_L[c]=fObjSel.options[a].text;
241 localArray_S[c]=1;
242 localArray_T[c] = fObjSel.options[a].title;
243 c++;
244 }
245 }
246 }
247 }
248 if (type=="Down") {
249 var tC = 0;
250 var tA = new Array();
251
252 for (a=0;a<l;a++) {
253 if (fObjSel.options[a].selected!=1) {
254 // Add non-selected element:
255 localArray_V[c]=fObjSel.options[a].value;
256 localArray_L[c]=fObjSel.options[a].text;
257 localArray_S[c]=0;
258 localArray_T[c] = fObjSel.options[a].title;
259 c++;
260
261 // Transfer any accumulated and reset:
262 if (tA.length > 0) {
263 for (aa=0;aa<tA.length;aa++) {
264 localArray_V[c]=fObjSel.options[tA[aa]].value;
265 localArray_L[c]=fObjSel.options[tA[aa]].text;
266 localArray_S[c]=1;
267 localArray_T[c] = fObjSel.options[tA[aa]].title;
268 c++;
269 }
270
271 var tC = 0;
272 var tA = new Array();
273 }
274 } else {
275 tA[tC] = a;
276 tC++;
277 }
278 }
279 // Transfer any remaining:
280 if (tA.length > 0) {
281 for (aa=0;aa<tA.length;aa++) {
282 localArray_V[c]=fObjSel.options[tA[aa]].value;
283 localArray_L[c]=fObjSel.options[tA[aa]].text;
284 localArray_S[c]=1;
285 localArray_T[c] = fObjSel.options[tA[aa]].title;
286 c++;
287 }
288 }
289 }
290 if (type=="Up") {
291 var tC = 0;
292 var tA = new Array();
293 var c = l-1;
294
295 for (a=l-1;a>=0;a--) {
296 if (fObjSel.options[a].selected!=1) {
297
298 // Add non-selected element:
299 localArray_V[c]=fObjSel.options[a].value;
300 localArray_L[c]=fObjSel.options[a].text;
301 localArray_S[c]=0;
302 localArray_T[c] = fObjSel.options[a].title;
303 c--;
304
305 // Transfer any accumulated and reset:
306 if (tA.length > 0) {
307 for (aa=0;aa<tA.length;aa++) {
308 localArray_V[c]=fObjSel.options[tA[aa]].value;
309 localArray_L[c]=fObjSel.options[tA[aa]].text;
310 localArray_S[c]=1;
311 localArray_T[c] = fObjSel.options[tA[aa]].title;
312 c--;
313 }
314
315 var tC = 0;
316 var tA = new Array();
317 }
318 } else {
319 tA[tC] = a;
320 tC++;
321 }
322 }
323 // Transfer any remaining:
324 if (tA.length > 0) {
325 for (aa=0;aa<tA.length;aa++) {
326 localArray_V[c]=fObjSel.options[tA[aa]].value;
327 localArray_L[c]=fObjSel.options[tA[aa]].text;
328 localArray_S[c]=1;
329 localArray_T[c] = fObjSel.options[tA[aa]].title;
330 c--;
331 }
332 }
333 c=l; // Restore length value in "c"
334 }
335
336 // Transfer items in temporary storage to list object:
337 fObjSel.length = c;
338 for (a = 0; a < c; a++) {
339 fObjSel.options[a].value = localArray_V[a];
340 fObjSel.options[a].text = localArray_L[a];
341 fObjSel.options[a].selected = localArray_S[a];
342 fObjSel.options[a].title = localArray_T[a];
343 }
344 FormEngine.updateHiddenFieldValueFromSelect(fObjSel, formObj[fName]);
345
346 FormEngine.legacyFieldChangedCb();
347 }
348 };
349
350
351 /**
352 * legacy function
353 * returns the DOM object for the given form name of the current form,
354 * but only if the given field name is valid, legacy function, use "getFormElement" instead
355 *
356 * @param fieldName the name of the field name
357 * @returns {*|DOMElement}
358 */
359 setFormValue_getFObj = function(fieldName) {
360 var $formEl = FormEngine.getFormElement(fieldName);
361 if ($formEl.length > 0) {
362 // return the DOM element of the form object
363 return $formEl.get(0);
364 } else {
365 return null;
366 }
367 };
368
369 /**
370 * returns a jQuery object for the given form name of the current form,
371 * if the parameter "fieldName" is given, then the form element is only returned if the field name is available
372 * the latter behaviour mirrors the one of the function "setFormValue_getFObj"
373 *
374 * @param fieldName the field name to check for, optional
375 * @returns {*|HTMLElement}
376 */
377 FormEngine.getFormElement = function(fieldName) {
378 var $formEl = $('form[name="' + FormEngine.formName + '"]:first');
379 if (fieldName) {
380 var $fieldEl = FormEngine.getFieldElement(fieldName)
381 ,$listFieldEl = FormEngine.getFieldElement(fieldName, '_list');
382
383 // Take the form object if it is either of type select-one or of type-multiple and it has a "_list" element
384 if ($fieldEl.length > 0 &&
385 (
386 ($fieldEl.prop('type') == 'select-one') ||
387 ($listFieldEl.length > 0 && $listFieldEl.prop('type').match(/select-(one|multiple)/))
388 )
389 ) {
390 return $formEl;
391 } else {
392 console.error('Form fields missing: form: ' + FormEngine.formName + ', field name: ' + fieldName);
393 alert('Form field is invalid');
394 }
395 } else {
396 return $formEl;
397 }
398 };
399
400
401 /**
402 * returns a jQuery object of the field DOM element of the current form, can also be used to
403 * request an alternative field like "_hr", "_list" or "_mul"
404 *
405 * @param fieldName the name of the field (<input name="fieldName">)
406 * @param appendix optional
407 * @param noFallback if set, then the appendix value is returned no matter if it exists or not
408 * @returns {*|HTMLElement}
409 */
410 FormEngine.getFieldElement = function(fieldName, appendix, noFallback) {
411 var $formEl = FormEngine.getFormElement();
412
413 // if an appendix is set, return the field with the appendix (like _mul or _list)
414 if (appendix) {
415 var $fieldEl = $(':input[name="' + fieldName + appendix + '"]', $formEl);
416 if ($fieldEl.length > 0 || noFallback === true) {
417 return $fieldEl;
418 }
419 }
420
421 return $(':input[name="' + fieldName + '"]', $formEl);
422 };
423
424
425
426 /**************************************************
427 * manipulate existing options in a select field
428 **************************************************/
429
430 /**
431 * moves currently selected options from a select field to the very top,
432 * can be multiple entries as well
433 *
434 * @param $fieldEl a jQuery object, containing the select field
435 */
436 FormEngine.moveOptionToTop = function($fieldEl) {
437 // remove the selected options
438 var selectedOptions = $fieldEl.find(':selected').detach();
439 // and add them on first position again
440 $fieldEl.prepend(selectedOptions);
441 };
442
443
444 /**
445 * moves currently selected options from a select field up by one position,
446 * can be multiple entries as well
447 *
448 * @param $fieldEl a jQuery object, containing the select field
449 */
450 FormEngine.moveOptionUp = function($fieldEl) {
451 // remove the selected options and add it before the previous sibling
452 $.each($fieldEl.find(':selected'), function(k, optionEl) {
453 var $optionEl = $(optionEl)
454 ,$optionBefore = $optionEl.prev();
455
456 // stop if first option to move is already the first one
457 if (k == 0 && $optionBefore.length === 0) {
458 return false;
459 }
460
461 $optionBefore.before($optionEl.detach());
462 });
463 };
464
465
466 /**
467 * moves currently selected options from a select field down one position,
468 * can be multiple entries as well
469 *
470 * @param $fieldEl a jQuery object, containing the select field
471 */
472 FormEngine.moveOptionDown = function($fieldEl) {
473 // remove the selected options and add it after the next sibling
474 // however, this time, we need to go from the last to the first
475 var selectedOptions = $fieldEl.find(':selected');
476 selectedOptions = $.makeArray(selectedOptions);
477 selectedOptions.reverse();
478 $.each(selectedOptions, function(k, optionEl) {
479 var $optionEl = $(optionEl)
480 ,$optionAfter = $optionEl.next();
481
482 // stop if first option to move is already the last one
483 if (k == 0 && $optionAfter.length === 0) {
484 return false;
485 }
486
487 $optionAfter.after($optionEl.detach());
488 });
489 };
490
491
492 /**
493 * moves currently selected options from a select field as the very last entries
494 *
495 * @param $fieldEl a jQuery object, containing the select field
496 */
497 FormEngine.moveOptionToBottom = function($fieldEl) {
498 // remove the selected options
499 var selectedOptions = $fieldEl.find(':selected').detach();
500 // and add them on last position again
501 $fieldEl.append(selectedOptions);
502 };
503
504 /**
505 * removes currently selected options from a select field
506 *
507 * @param $fieldEl a jQuery object, containing the select field
508 */
509 FormEngine.removeOption = function($fieldEl) {
510 // remove the selected options
511 $fieldEl.find(':selected').remove();
512 };
513
514
515 /**
516 * initialize events for all form engine relevant tasks
517 * this function only needs to be called once on page load,
518 * as it using deferrer methods only
519 */
520 FormEngine.initializeEvents = function() {
521
522 // track the arrows "Up", "Down", "Clear" etc in multi-select boxes
523 $(document).on('click', '.t3-btn-moveoption-top, .t3-btn-moveoption-up, .t3-btn-moveoption-down, .t3-btn-moveoption-bottom, .t3-btn-removeoption', function(evt) {
524 var $el = $(this)
525 ,fieldName = $el.data('fieldname')
526 ,$listFieldEl = FormEngine.getFieldElement(fieldName, '_list');
527
528 if ($listFieldEl.length > 0) {
529
530 if ($el.hasClass('t3-btn-moveoption-top')) {
531 FormEngine.moveOptionToTop($listFieldEl);
532 } else if ($el.hasClass('t3-btn-moveoption-up')) {
533 FormEngine.moveOptionUp($listFieldEl);
534 } else if ($el.hasClass('t3-btn-moveoption-down')) {
535 FormEngine.moveOptionDown($listFieldEl);
536 } else if ($el.hasClass('t3-btn-moveoption-bottom')) {
537 FormEngine.moveOptionToBottom($listFieldEl);
538 } else if ($el.hasClass('t3-btn-removeoption')) {
539 FormEngine.removeOption($listFieldEl);
540 }
541
542 // make sure to update the hidden field value when modifying the select value
543 FormEngine.updateHiddenFieldValueFromSelect($listFieldEl, FormEngine.getFieldElement(fieldName));
544 FormEngine.legacyFieldChangedCb();
545 }
546 });
547
548 // in multi-select environments with two (e.g. "Access"), on click the item from the right should go to the left
549 $(document).on('click', '.t3-form-select-itemstoselect', function(evt) {
550 var $el = $(this)
551 ,fieldName = $el.data('relatedfieldname')
552 ,exclusiveValues = $el.data('exclusivevalues');
553
554 if (fieldName) {
555 // try to add each selected field to the "left" select field
556 $el.find(':selected').each(function() {
557 var $optionEl = $(this);
558 FormEngine.setSelectOptionFromExternalSource(fieldName, $optionEl.prop('value'), $optionEl.text(), $optionEl.prop('title'), exclusiveValues);
559 });
560 }
561 });
562 };
563
564 /**
565 * select field filter functions, see TCA option "enableMultiSelectFilterTextfield"
566 * and "multiSelectFilterItems"
567 */
568 FormEngine.SelectBoxFilter = {
569 options: {
570 fieldContainerSelector: '.t3-form-field-group-file',
571 filterContainerSelector: '.t3-form-multiselect-filter-container',
572 filterTextFieldSelector: '.t3-form-multiselect-filter-textfield',
573 filterSelectFieldSelector: '.t3-form-multiselect-filter-dropdown',
574 itemsToSelectElementSelector: '.t3-form-select-itemstoselect'
575 }
576 };
577
578 /**
579 * make sure that all selectors and input filters are recognized
580 * note: this also works on elements that are loaded asynchronously via AJAX, no need to call this method
581 * after an AJAX load.
582 */
583 FormEngine.SelectBoxFilter.initializeEvents = function() {
584 $(document).on('keyup', FormEngine.SelectBoxFilter.options.filterTextFieldSelector, function() {
585 var $selectElement = FormEngine.SelectBoxFilter.getSelectElement($(this));
586 FormEngine.SelectBoxFilter.filter($selectElement, $(this).val());
587 }).on('change', FormEngine.SelectBoxFilter.options.filterSelectFieldSelector, function() {
588 var $selectElement = FormEngine.SelectBoxFilter.getSelectElement($(this));
589 FormEngine.SelectBoxFilter.filter($selectElement, $(this).val());
590 });
591 };
592
593 /**
594 * fetch the "itemstoselect" select element where a filter item is attached to
595 */
596 FormEngine.SelectBoxFilter.getSelectElement = function($relativeElement) {
597 var $containerElement = $relativeElement.closest(FormEngine.SelectBoxFilter.options.fieldContainerSelector);
598 return $containerElement.find(FormEngine.SelectBoxFilter.options.itemsToSelectElementSelector);
599 };
600
601 /**
602 * filter the actual items
603 */
604 FormEngine.SelectBoxFilter.filter = function($selectElement, filterText) {
605 var $allOptionElements;
606 if (!$selectElement.data('alloptions')) {
607 $allOptionElements = $selectElement.find('option').clone();
608 $selectElement.data('alloptions', $allOptionElements);
609 } else {
610 $allOptionElements = $selectElement.data('alloptions');
611 }
612
613 if (filterText.length > 0) {
614 var matchFilter = new RegExp(filterText, 'i');
615 $selectElement.html('');
616 $allOptionElements.each(function() {
617 if ($(this).text().match(matchFilter)) {
618 $selectElement.append($(this).clone());
619 }
620 });
621 } else {
622 $selectElement.html($allOptionElements);
623 }
624 };
625
626 /**
627 * this is the main function that is called on page load, but also after elements are asynchroniously
628 * called e.g. after IRRE elements are loaded again, or a new flexform section is added.
629 * use this function in your extension like this "TYPO3.FormEngine.initialize()"
630 * if you add new fields dynamically.
631 *
632 */
633 FormEngine.reinitialize = function() {
634 // apply "close" button to all input / datetime fields
635 if ($('.t3-tceforms-input-wrapper, .t3-tceforms-input-wrapper-datetime').length) {
636 require(['jquery/jquery.clearable'], function() {
637 $('.t3-tceforms-input-wrapper .formfield-input, .t3-tceforms-input-wrapper-datetime .formfield-input').clearable();
638 });
639 }
640 // apply DatePicker to all date time fields
641 require(['TYPO3/CMS/Backend/DateTimePicker']);
642 };
643
644 /**
645 * initialize function, always require possible post-render hooks return the main object
646 */
647 return function() {
648 // the functions are both using delegates, thus no need to be called again
649 FormEngine.initializeEvents();
650 FormEngine.SelectBoxFilter.initializeEvents();
651 FormEngine.reinitialize();
652
653 // load required modules to hook in the post initialize function
654 if (undefined !== TYPO3.settings.RequireJS && undefined !== TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/FormEngine']) {
655 $.each(TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/FormEngine'], function(pos, moduleName) {
656 require([moduleName]);
657 });
658 }
659
660 // make the form engine object publically visible for other objects in the TYPO3 namespace
661 TYPO3.FormEngine = FormEngine;
662
663 // return the object in the global space
664 return FormEngine;
665 }();
666 });