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