2273365d45dbef39f2e97fa5de45fad438fb744d
[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 * @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 $(document).on('change', 'input,textarea,select', function() {
594 // Keep track of input fields to set the dirty state
595 FormEngine.isDirty = $(document).find('.has-change').length > 0;
596 }).on('click', '.t3js-btn-moveoption-top, .t3js-btn-moveoption-up, .t3js-btn-moveoption-down, .t3js-btn-moveoption-bottom, .t3js-btn-removeoption', function(evt) {
597 evt.preventDefault();
598
599 // track the arrows "Up", "Down", "Clear" etc in multi-select boxes
600 var $el = $(this)
601 ,fieldName = $el.data('fieldname')
602 ,$listFieldEl = FormEngine.getFieldElement(fieldName, '_list');
603
604 if ($listFieldEl.length > 0) {
605
606 if ($el.hasClass('t3js-btn-moveoption-top')) {
607 FormEngine.moveOptionToTop($listFieldEl);
608 } else if ($el.hasClass('t3js-btn-moveoption-up')) {
609 FormEngine.moveOptionUp($listFieldEl);
610 } else if ($el.hasClass('t3js-btn-moveoption-down')) {
611 FormEngine.moveOptionDown($listFieldEl);
612 } else if ($el.hasClass('t3js-btn-moveoption-bottom')) {
613 FormEngine.moveOptionToBottom($listFieldEl);
614 } else if ($el.hasClass('t3js-btn-removeoption')) {
615 var $availableFieldEl = FormEngine.getFieldElement(fieldName, '_avail');
616 FormEngine.removeOption($listFieldEl, $availableFieldEl);
617 }
618
619 // make sure to update the hidden field value when modifying the select value
620 FormEngine.updateHiddenFieldValueFromSelect($listFieldEl, FormEngine.getFieldElement(fieldName));
621 FormEngine.legacyFieldChangedCb();
622 if (typeof FormEngine.Validation !== 'undefined' && typeof FormEngine.Validation.validate === 'function') {
623 FormEngine.Validation.validate();
624 }
625 }
626 }).on('click', '.t3js-formengine-select-itemstoselect', function(evt) {
627 // in multi-select environments with two (e.g. "Access"), on click the item from the right should go to the left
628 var $el = $(this)
629 ,fieldName = $el.data('relatedfieldname')
630 ,exclusiveValues = $el.data('exclusivevalues');
631
632 if (fieldName) {
633 // try to add each selected field to the "left" select field
634 $el.find(':selected').each(function() {
635 var $optionEl = $(this);
636 FormEngine.setSelectOptionFromExternalSource(fieldName, $optionEl.prop('value'), $optionEl.text(), $optionEl.prop('title'), exclusiveValues, $optionEl);
637 });
638 }
639 }).on('click', '.t3js-editform-close', function(e) {
640 e.preventDefault();
641 FormEngine.preventExitIfNotSaved();
642 }).on('click', '.t3js-editform-delete-record', function(e) {
643 e.preventDefault();
644 var title = TYPO3.lang['label.confirm.delete_record.title'] || 'Delete this record?';
645 var content = TYPO3.lang['label.confirm.delete_record.content'] || 'Are you sure you want to delete this record?';
646 var $anchorElement = $(this);
647 var $modal = Modal.confirm(title, content, Severity.warning, [
648 {
649 text: TYPO3.lang['buttons.confirm.delete_record.no'] || 'Cancel',
650 active: true,
651 btnClass: 'btn-default',
652 name: 'no'
653 },
654 {
655 text: TYPO3.lang['buttons.confirm.delete_record.yes'] || 'Yes, delete this record',
656 btnClass: 'btn-warning',
657 name: 'yes'
658 }
659 ]);
660 $modal.on('button.clicked', function(e) {
661 if (e.target.name === 'no') {
662 Modal.dismiss();
663 } else if (e.target.name === 'yes') {
664 deleteRecord($anchorElement.data('table'), $anchorElement.data('uid'), $anchorElement.data('return-url'));
665 Modal.dismiss();
666 }
667 });
668 }).on('click', '.t3js-editform-delete-inline-record', function(e) {
669 e.preventDefault();
670 var title = TYPO3.lang['label.confirm.delete_record.title'] || 'Delete this record?';
671 var content = TYPO3.lang['label.confirm.delete_record.content'] || 'Are you sure you want to delete this record?';
672 var $anchorElement = $(this);
673 var $modal = Modal.confirm(title, content, Severity.warning, [
674 {
675 text: TYPO3.lang['buttons.confirm.delete_record.no'] || 'Cancel',
676 active: true,
677 btnClass: 'btn-default',
678 name: 'no'
679 },
680 {
681 text: TYPO3.lang['buttons.confirm.delete_record.yes'] || 'Yes, delete this record',
682 btnClass: 'btn-warning',
683 name: 'yes'
684 }
685 ]);
686 $modal.on('button.clicked', function(e) {
687 if (e.target.name === 'no') {
688 Modal.dismiss();
689 } else if (e.target.name === 'yes') {
690 var objectId = $anchorElement.data('objectid');
691 inline.deleteRecord(objectId);
692 Modal.dismiss();
693 }
694 });
695 }).on('click', '.t3js-editform-submitButton', function(event) {
696 // remember the clicked submit button. we need to know that in TBE_EDITOR.submitForm();
697 var $me = $(this),
698 name = $me.data('name') || this.name,
699 $elem = $('<input />').attr('type', 'hidden').attr('name', name).attr('value', '1');
700
701 $me.parents('form').append($elem);
702 });
703 };
704
705 /**
706 * Initializes the remaining character views based on the fields' maxlength attribute
707 */
708 FormEngine.initializeRemainingCharacterViews = function() {
709 // all fields with a "maxlength" attribute
710 var $maxlengthElements = $('[maxlength]').not('.t3js-datetimepicker');
711 $maxlengthElements.on('focus', function(e) {
712 var $field = $(this),
713 $parent = $field.parents('.t3js-formengine-field-item:first'),
714 maxlengthProperties = FormEngine.getCharacterCounterProperties($field);
715
716 // append the counter only at focus to avoid cluttering the DOM
717 $parent.append($('<div />', {'class': 't3js-charcounter'}).append(
718 $('<span />', {'class': maxlengthProperties.labelClass}).text(TBE_EDITOR.labels.remainingCharacters.replace('{0}', maxlengthProperties.remainingCharacters))
719 ));
720 }).on('blur', function() {
721 var $field = $(this),
722 $parent = $field.parents('.t3js-formengine-field-item:first');
723 $parent.find('.t3js-charcounter').remove();
724 }).on('keyup', function() {
725 var $field = $(this),
726 $parent = $field.parents('.t3js-formengine-field-item:first'),
727 maxlengthProperties = FormEngine.getCharacterCounterProperties($field);
728
729 // change class and value
730 $parent.find('.t3js-charcounter span').removeClass().addClass(maxlengthProperties.labelClass).text(TBE_EDITOR.labels.remainingCharacters.replace('{0}', maxlengthProperties.remainingCharacters))
731 });
732 $(':password').on('focus', function() {
733 $(this).attr('type', 'text').select();
734 }).on('blur', function() {
735 $(this).attr('type', 'password');
736 });
737 };
738
739 /**
740 * Initialize select checkbox element checkboxes
741 */
742 FormEngine.initializeSelectCheckboxes = function() {
743 $('.t3js-toggle-checkboxes').each(function() {
744 var $checkbox = $(this);
745 var $table = $checkbox.closest('table');
746 var $checkboxes = $table.find('.t3js-checkbox');
747 var checkIt = $checkboxes.length === $table.find('.t3js-checkbox:checked').length;
748 $checkbox.prop('checked', checkIt);
749 });
750 $(document).on('change', '.t3js-toggle-checkboxes', function(e) {
751 e.preventDefault();
752 var $checkbox = $(this);
753 var $table = $checkbox.closest('table');
754 var $checkboxes = $table.find('.t3js-checkbox');
755 var checkIt = $checkboxes.length !== $table.find('.t3js-checkbox:checked').length;
756 $checkboxes.prop('checked', checkIt);
757 $checkbox.prop('checked', checkIt);
758 });
759 $(document).on('change', '.t3js-checkbox', function(e) {
760 FormEngine.updateCheckboxState(this);
761 });
762 };
763
764 /**
765 *
766 * @param {HTMLElement} source
767 */
768 FormEngine.updateCheckboxState = function(source) {
769 var $sourceElement = $(source);
770 var $table = $sourceElement.closest('table');
771 var $checkboxes = $table.find('.t3js-checkbox');
772 var checkIt = $checkboxes.length === $table.find('.t3js-checkbox:checked').length;
773 $table.find('.t3js-toggle-checkboxes').prop('checked', checkIt);
774 };
775
776 /**
777 * Get the properties required for proper rendering of the character counter
778 *
779 * @param {Object} $field
780 * @returns {{remainingCharacters: number, labelClass: string}}
781 */
782 FormEngine.getCharacterCounterProperties = function($field) {
783 var fieldText = $field.val(),
784 maxlength = $field.attr('maxlength'),
785 currentFieldLength = fieldText.length,
786 numberOfLineBreaks = (fieldText.match(/\n/g)||[]).length, // count line breaks
787 remainingCharacters = maxlength - currentFieldLength - numberOfLineBreaks,
788 threshold = 15, // hard limit of remaining characters when the label class changes
789 labelClass = '';
790
791 if (remainingCharacters < threshold) {
792 labelClass = 'label-danger';
793 } else if(remainingCharacters < threshold * 2) {
794 labelClass = 'label-warning';
795 } else {
796 labelClass = 'label-info';
797 }
798
799 return {
800 remainingCharacters: remainingCharacters,
801 labelClass: 'label ' + labelClass
802 };
803 };
804
805 /**
806 * Select field filter functions, see TCA option "enableMultiSelectFilterTextfield"
807 * and "multiSelectFilterItems"
808 */
809 FormEngine.SelectBoxFilter = {
810 options: {
811 fieldContainerSelector: '.t3js-formengine-field-group',
812 filterContainerSelector: '.t3js-formengine-multiselect-filter-container',
813 filterTextFieldSelector: '.t3js-formengine-multiselect-filter-textfield',
814 filterSelectFieldSelector: '.t3js-formengine-multiselect-filter-dropdown',
815 itemsToSelectElementSelector: '.t3js-formengine-select-itemstoselect'
816 }
817 };
818
819 /**
820 * Make sure that all selectors and input filters are recognized
821 * note: this also works on elements that are loaded asynchronously via AJAX, no need to call this method
822 * after an AJAX load.
823 */
824 FormEngine.SelectBoxFilter.initializeEvents = function() {
825 $(document).on('keyup', FormEngine.SelectBoxFilter.options.filterTextFieldSelector, function() {
826 var $selectElement = FormEngine.SelectBoxFilter.getSelectElement($(this));
827 FormEngine.SelectBoxFilter.filter($selectElement, $(this).val());
828 }).on('change', FormEngine.SelectBoxFilter.options.filterSelectFieldSelector, function() {
829 var $selectElement = FormEngine.SelectBoxFilter.getSelectElement($(this));
830 FormEngine.SelectBoxFilter.filter($selectElement, $(this).val());
831 });
832 };
833
834 /**
835 * Fetch the "itemstoselect" select element where a filter item is attached to
836 *
837 * @param {Object} $relativeElement
838 * @returns {*}
839 */
840 FormEngine.SelectBoxFilter.getSelectElement = function($relativeElement) {
841 var $containerElement = $relativeElement.closest(FormEngine.SelectBoxFilter.options.fieldContainerSelector);
842 return $containerElement.find(FormEngine.SelectBoxFilter.options.itemsToSelectElementSelector);
843 };
844
845 /**
846 * Filter the actual items
847 *
848 * @param {Object} $selectElement
849 * @param {String} filterText
850 */
851 FormEngine.SelectBoxFilter.filter = function($selectElement, filterText) {
852 var $allOptionElements;
853 if (!$selectElement.data('alloptions')) {
854 $allOptionElements = $selectElement.find('option').clone();
855 $selectElement.data('alloptions', $allOptionElements);
856 } else {
857 $allOptionElements = $selectElement.data('alloptions');
858 }
859
860 if (filterText.length > 0) {
861 var matchFilter = new RegExp(filterText, 'i');
862 $selectElement.html('');
863 $allOptionElements.each(function() {
864 var $item = $(this);
865 if ($item.text().match(matchFilter)) {
866 $selectElement.append($item.clone());
867 }
868 });
869 } else {
870 $selectElement.html($allOptionElements);
871 }
872 };
873
874 /**
875 * convert all textareas so they grow when it is typed in.
876 */
877 FormEngine.convertTextareasResizable = function() {
878 var $elements = $('.t3js-formengine-textarea');
879 if (TYPO3.settings.Textarea && TYPO3.settings.Textarea.autosize && $elements.length) {
880 require(['autosize'], function(autosize) {
881 autosize($elements);
882 });
883 }
884 };
885
886 /**
887 * convert all textareas to enable tab
888 */
889 FormEngine.convertTextareasEnableTab = function() {
890 var $elements = $('.t3js-enable-tab');
891 if ($elements.length) {
892 require(['taboverride'], function(taboverride) {
893 taboverride.set($elements);
894 });
895 }
896 };
897
898 /**
899 * this is the main function that is called on page load, but also after elements are asynchroniously
900 * called e.g. after IRRE elements are loaded again, or a new flexform section is added.
901 * use this function in your extension like this "TYPO3.FormEngine.initialize()"
902 * if you add new fields dynamically.
903 *
904 */
905 FormEngine.reinitialize = function() {
906 // apply "close" button to all input / datetime fields
907 if ($('.t3js-clearable').length) {
908 require(['TYPO3/CMS/Backend/jquery.clearable'], function() {
909 $('.t3js-clearable').clearable();
910 });
911 }
912 if ($('.t3-form-suggest').length) {
913 require(['TYPO3/CMS/Backend/FormEngineSuggest'], function(Suggest) {
914 Suggest($('.t3-form-suggest'));
915 });
916 }
917 // apply DatePicker to all date time fields
918 require(['TYPO3/CMS/Backend/DateTimePicker'], function(DateTimePicker) {
919 DateTimePicker.initialize();
920 });
921
922 FormEngine.convertTextareasResizable();
923 FormEngine.convertTextareasEnableTab();
924 };
925
926 /**
927 * Show modal to confirm closing the document without saving
928 */
929 FormEngine.preventExitIfNotSaved = function() {
930 if (FormEngine.isDirty) {
931 var title = TYPO3.lang['label.confirm.close_without_save.title'] || 'Do you want to quit without saving?';
932 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?';
933 var $modal = Modal.confirm(title, content, Severity.warning, [
934 {
935 text: TYPO3.lang['buttons.confirm.close_without_save.no'] || 'No, I will continue editing',
936 active: true,
937 btnClass: 'btn-default',
938 name: 'no'
939 },
940 {
941 text: TYPO3.lang['buttons.confirm.close_without_save.yes'] || 'Yes, discard my changes',
942 btnClass: 'btn-warning',
943 name: 'yes'
944 }
945 ]);
946 $modal.on('button.clicked', function(e) {
947 if (e.target.name === 'no') {
948 Modal.dismiss();
949 } else if (e.target.name === 'yes') {
950 Modal.dismiss();
951 FormEngine.closeDocument();
952 }
953 });
954 } else {
955 FormEngine.closeDocument()
956 }
957 };
958
959 /**
960 * Show modal to confirm closing the document without saving
961 */
962 FormEngine.preventSaveIfHasErrors = function() {
963 if ($('.has-error').length > 0) {
964 var title = TYPO3.lang['label.alert.save_with_error.title'] || 'You have errors in your form!';
965 var content = TYPO3.lang['label.alert.save_with_error.content'] || 'Please check the form, there is at least one error in your form.';
966 var $modal = Modal.confirm(title, content, Severity.error, [
967 {
968 text: TYPO3.lang['buttons.alert.save_with_error.ok'] || 'OK',
969 btnClass: 'btn-danger',
970 name: 'ok'
971 }
972 ]);
973 $modal.on('button.clicked', function(e) {
974 if (e.target.name === 'ok') {
975 Modal.dismiss();
976 }
977 });
978 return false;
979 }
980 return true;
981 };
982
983 /**
984 * Close current open document
985 */
986 FormEngine.closeDocument = function() {
987 document.editform.closeDoc.value=1;
988 document.editform.submit();
989 };
990
991 /**
992 * initialize function, always require possible post-render hooks return the main object
993 */
994
995 // the functions are both using delegates, thus no need to be called again
996 FormEngine.initializeEvents();
997 FormEngine.SelectBoxFilter.initializeEvents();
998 FormEngine.reinitialize();
999
1000 // load required modules to hook in the post initialize function
1001 if (undefined !== TYPO3.settings.RequireJS && undefined !== TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/FormEngine']) {
1002 $.each(TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/FormEngine'], function(pos, moduleName) {
1003 require([moduleName]);
1004 });
1005 }
1006
1007 // make the form engine object publically visible for other objects in the TYPO3 namespace
1008 TYPO3.FormEngine = FormEngine;
1009
1010 // return the object in the global space
1011 return FormEngine;
1012 });