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