3e2f232889fcb6812a2495f54c2d98f97973e57a
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Public / JavaScript / jsfunc.inline.js
1 /*<![CDATA[*/
2
3 /*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16 /**
17 * Inline-Relational-Record Editing
18 */
19
20 var inline = {
21 classVisible: 'panel-visible',
22 classCollapsed: 'panel-collapsed',
23 structureSeparator: '-',
24 flexFormSeparator: '---',
25 flexFormSubstitute: ':',
26 noTitleString: (TYPO3.lang ? TYPO3.lang['FormEngine.noRecordTitle'] : '[No title]'),
27 lockedAjaxMethod: {},
28 sourcesLoaded: {},
29 data: {},
30 isLoading: false,
31
32 addToDataArray: function (object) {
33 $.each(object, function (key, value) {
34 if (!inline.data[key]) {
35 inline.data[key] = {};
36 }
37 $.extend(inline.data[key], value);
38 });
39 },
40 toggleEvent: function (event) {
41 var $triggerElement = $(event.target);
42 if ($triggerElement.parents('.t3js-formengine-irre-control').length == 1) {
43 return;
44 }
45
46 var $recordHeader = $triggerElement.closest('.panel-heading');
47 inline.expandCollapseRecord(
48 $recordHeader.attr('id').replace(/_header$/, ''),
49 $recordHeader.attr('data-expandSingle')
50 );
51 },
52 expandCollapseRecord: function (objectId, expandSingle) {
53 var currentUid = this.parseObjectId('none', objectId, 1);
54 var objectPrefix = this.parseObjectId('full', objectId, 0, 1);
55 var escapedObjectId = this.escapeObjectId(objectId);
56
57 var $currentObject = $('#' + escapedObjectId + '_div');
58 // if content is not loaded yet, get it now from server
59 if (inline.isLoading) {
60 return false;
61 } else if ($('#' + escapedObjectId + '_fields').length > 0 && $('#' + escapedObjectId + '_fields').html().substr(0, 16) === '<!--notloaded-->') {
62 inline.isLoading = true;
63 var headerIdentifier = '#' + escapedObjectId + '_header';
64 // add loading-indicator
65 require(['nprogress'], function (NProgress) {
66 inline.progress = NProgress;
67 inline.progress.configure({parent: headerIdentifier, showSpinner: false});
68 inline.progress.start();
69 });
70 return this.getRecordDetails(objectId);
71 }
72
73 var isCollapsed = $currentObject.hasClass(this.classCollapsed);
74 var collapse = [];
75 var expand = [];
76
77 // if only a single record should be visibly for that set of records
78 // and the record clicked itself is no visible, collapse all others
79 if (expandSingle && $currentObject.hasClass(this.classCollapsed)) {
80 collapse = this.collapseAllRecords(objectId, objectPrefix, currentUid);
81 }
82
83 inline.toggleElement(objectId);
84
85 if (this.isNewRecord(objectId)) {
86 this.updateExpandedCollapsedStateLocally(objectId, isCollapsed);
87 } else if (isCollapsed) {
88 expand.push(currentUid);
89 } else if (!isCollapsed) {
90 collapse.push(currentUid);
91 }
92
93 this.setExpandedCollapsedState(objectId, expand.join(','), collapse.join(','));
94
95 return false;
96 },
97
98 toggleElement: function (objectId) {
99 var escapedObjectId = this.escapeObjectId(objectId);
100 var $jQueryObject = $('#' + escapedObjectId + '_div');
101
102 if ($jQueryObject.hasClass(this.classCollapsed)) {
103 $jQueryObject.removeClass(this.classCollapsed).addClass(this.classVisible);
104 $jQueryObject.find('#' + escapedObjectId + '_header .t3-icon-irre-collapsed').removeClass('t3-icon-irre-collapsed').addClass('t3-icon-irre-expanded');
105 } else {
106 $jQueryObject.removeClass(this.classVisible).addClass(this.classCollapsed);
107 $jQueryObject.find('#' + escapedObjectId + '_header .t3-icon-irre-expanded').addClass('t3-icon-irre-collapsed').removeClass('t3-icon-irre-expanded');
108 }
109 },
110 collapseAllRecords: function (objectId, objectPrefix, callingUid) {
111 // get the form field, where all records are stored
112 var objectName = 'data' + this.parseObjectId('parts', objectId, 3, 2, true);
113 var formObj = document.getElementsByName(objectName);
114 var collapse = [];
115
116 if (formObj.length) {
117 // the uid of the calling object (last part in objectId)
118 var recObjectId = '', escapedRecordObjectId;
119
120 var records = this.trimExplode(',', formObj[0].value);
121 for (var i = 0; i < records.length; i++) {
122 recObjectId = objectPrefix + this.structureSeparator + records[i];
123 escapedRecordObjectId = this.escapeObjectId(recObjectId);
124
125 var $recordEntry = $('#' + escapedRecordObjectId + '_div');
126 if (records[i] != callingUid && $recordEntry.hasClass(this.classVisible)) {
127 $recordEntry.removeClass(this.classVisible).addClass(this.classCollapsed);
128 if (this.isNewRecord(recObjectId)) {
129 this.updateExpandedCollapsedStateLocally(recObjectId, 0);
130 } else {
131 collapse.push(records[i]);
132 }
133 }
134 }
135 }
136
137 return collapse;
138 },
139
140 updateExpandedCollapsedStateLocally: function (objectId, value) {
141 var ucName = 'uc[inlineView]' + this.parseObjectId('parts', objectId, 3, 2, true);
142 var ucFormObj = document.getElementsByName(ucName);
143 if (ucFormObj.length) {
144 ucFormObj[0].value = value;
145 }
146 },
147
148 getRecordDetails: function (objectId) {
149 var context = this.getContext(this.parseObjectId('full', objectId, 0, 1));
150 inline.makeAjaxCall('details', [objectId], true, context);
151 return false;
152 },
153
154 createNewRecord: function (objectId, recordUid) {
155 if (this.isBelowMax(objectId)) {
156 var context = this.getContext(objectId);
157 if (recordUid) {
158 objectId += this.structureSeparator + recordUid;
159 }
160 this.makeAjaxCall('create', [objectId], true, context);
161 } else {
162 var message = TYPO3.lang['FormEngine.maxItemsAllowed'].replace('{0}', this.data.config[objectId].max);
163 var matches = objectId.match(/^(data-\d+-.*?-\d+-.*?)-(.*?)$/);
164 var title = '';
165 if (matches) {
166 title = $('#' + matches[1] + '_records').data('title');
167 }
168 top.TYPO3.Notification.error(title, message, 5);
169 }
170 return false;
171 },
172
173 synchronizeLocalizeRecords: function (objectId, type) {
174 var context = this.getContext(objectId);
175 this.makeAjaxCall('synchronizelocalize', [objectId, type], true, context);
176 return false;
177 },
178
179 setExpandedCollapsedState: function (objectId, expand, collapse) {
180 var context = this.getContext(objectId);
181 this.makeAjaxCall('expandcollapse', [objectId, expand, collapse], false, context);
182 },
183
184 makeAjaxCall: function (method, params, lock, context) {
185 var url = '', urlParams = '', options = {};
186 if (method && params && params.length && this.lockAjaxMethod(method, lock)) {
187 url = TYPO3.settings.ajaxUrls['record_inline_' + method];
188 urlParams = '';
189 for (var i = 0; i < params.length; i++) {
190 urlParams += '&ajax[' + i + ']=' + encodeURIComponent(params[i]);
191 }
192 if (context) {
193 urlParams += '&ajax[context]=' + encodeURIComponent(JSON.stringify(context));
194 }
195 options = {
196 type: 'POST',
197 data: urlParams,
198 success: function (data, message, jqXHR) {
199 inline.isLoading = false;
200 inline.processAjaxResponse(method, jqXHR);
201 if (inline.progress) {
202 inline.progress.done();
203 }
204 },
205 error: function (jqXHR) {
206 inline.isLoading = false;
207 inline.showAjaxFailure(method, jqXHR);
208 if (inline.progress) {
209 inline.progress.done();
210 }
211 }
212 };
213
214 $.ajax(url, options);
215 }
216 },
217
218 lockAjaxMethod: function (method, lock) {
219 if (!lock || !inline.lockedAjaxMethod[method]) {
220 inline.lockedAjaxMethod[method] = true;
221 return true;
222 } else {
223 return false;
224 }
225 },
226
227 unlockAjaxMethod: function (method) {
228 inline.lockedAjaxMethod[method] = false;
229 },
230
231 processAjaxResponse: function (method, xhr, json) {
232 var processedCount = 0, sourcesWaiting = [];
233 if (!json && xhr) {
234 json = xhr.responseJSON;
235 }
236 // If there are elements the should be added to the <HEAD> tag (e.g. for RTEhtmlarea):
237 if (json.stylesheetFiles) {
238 $.each(json.stylesheetFiles, function (index, stylesheetFile) {
239 if (!stylesheetFile) {
240 return;
241 }
242 var element = document.createElement('link');
243 element['rel'] = 'stylesheet';
244 element['type'] = 'text/css';
245 element['href'] = stylesheetFile;
246 $('head').get(0).appendChild(element);
247 processedCount++;
248 delete(json.stylesheetFiles[index]);
249 });
250 }
251 if (processedCount) {
252 window.setTimeout(function () {
253 inline.reprocessAjaxResponse(method, json, sourcesWaiting);
254 }, 40);
255 } else {
256 if (method) {
257 inline.unlockAjaxMethod(method);
258 }
259 if (json.scriptCall && json.scriptCall.length > 0) {
260 $.each(json.scriptCall, function (index, value) {
261 eval(value);
262 });
263 }
264 TYPO3.FormEngine.reinitialize();
265 TYPO3.FormEngine.Validation.initializeInputFields();
266 TYPO3.FormEngine.Validation.validate();
267 }
268 },
269
270 // Check if dynamically added scripts are loaded and restart inline.processAjaxResponse():
271 reprocessAjaxResponse: function (method, json, sourcesWaiting) {
272 var sourcesLoaded = true;
273 if (sourcesWaiting && sourcesWaiting.length) {
274 $.each(sourcesWaiting, function (index, source) {
275 if (!inline.sourcesLoaded[source]) {
276 sourcesLoaded = false;
277 return false;
278 }
279 });
280 }
281 if (sourcesLoaded) {
282 $.each(sourcesWaiting, function (index, source) {
283 delete(inline.sourcesLoaded[source]);
284 });
285 window.setTimeout(function () {
286 inline.processAjaxResponse(method, null, json);
287 }, 80);
288 } else {
289 window.setTimeout(function () {
290 inline.reprocessAjaxResponse(method, json, sourcesWaiting);
291 }, 40);
292 }
293 },
294
295 sourceLoadedHandler: function (element) {
296 if (element && element.src) {
297 inline.sourcesLoaded[element.src] = true;
298 }
299 },
300
301 showAjaxFailure: function (method, xhr) {
302 inline.unlockAjaxMethod(method);
303 top.TYPO3.Notification.error('Error ' + xhr.status, xhr.statusText, 0);
304 },
305
306 // foreign_selector: used by selector box (type='select')
307 importNewRecord: function (objectId) {
308 var $selector = $('#' + this.escapeObjectId(objectId) + '_selector');
309 var selectedIndex = $selector.prop('selectedIndex');
310 if (selectedIndex != -1) {
311 var context = this.getContext(objectId);
312 var selectedValue = $selector.val();
313 if (!this.data.unique || !this.data.unique[objectId]) {
314 $selector.find('option').eq(selectedIndex).prop('selected', false);
315 }
316 this.makeAjaxCall('create', [objectId, selectedValue], true, context);
317 }
318 return false;
319 },
320
321 // foreign_selector: used by element browser (type='group/db')
322 importElement: function (objectId, table, uid) {
323 var context = this.getContext(objectId);
324 inline.makeAjaxCall('create', [objectId, uid], true, context);
325 },
326
327 importElementMultiple: function (objectId, table, uidArray, type) {
328 $.each(uidArray, function (index, uid) {
329 inline.delayedImportElement(objectId, table, uid, type);
330 });
331 },
332 delayedImportElement: function (objectId, table, uid, type) {
333 if (inline.lockedAjaxMethod['create'] == true) {
334 window.setTimeout("inline.delayedImportElement('" + objectId + "','" + table + "'," + uid + ", null );",
335 300);
336 } else {
337 inline.importElement(objectId, table, uid, type);
338 }
339 },
340 // Check uniqueness for element browser:
341 checkUniqueElement: function (objectId, table, uid) {
342 if (this.checkUniqueUsed(objectId, uid, table)) {
343 return {passed: false, message: 'There is already a relation to the selected element!'};
344 } else {
345 return {passed: true};
346 }
347 },
348
349 // Checks if a record was used and should be unique:
350 checkUniqueUsed: function (objectId, uid, table) {
351 if (!this.data.unique || !this.data.unique[objectId]) {
352 return false;
353 }
354
355 var unique = this.data.unique[objectId];
356 var values = this.getValuesFromHashMap(unique.used);
357
358 // for select: only the uid is stored
359 if (unique['type'] == 'select') {
360 if (values.indexOf(uid) != -1) {
361 return true;
362 }
363
364 // for group/db: table and uid is stored in an assoc array
365 } else if (unique.type == 'groupdb') {
366 for (var i = values.length - 1; i >= 0; i--) {
367 // if the pair table:uid is already used:
368 if (values[i].table == table && values[i].uid == uid) {
369 return true;
370 }
371 }
372 }
373
374 return false;
375 },
376
377 setUniqueElement: function (objectId, table, uid, type, elName) {
378 var recordUid = this.parseFormElementName('none', elName, 1, 1);
379 this.setUnique(objectId, recordUid, uid);
380 },
381
382 getValuesFromHashMap: function (hashMap) {
383 return $.map(hashMap, function (value, key) {
384 return value;
385 });
386 },
387
388 // Remove all select items already used
389 // from a newly retrieved/expanded record
390 removeUsed: function (objectId, recordUid) {
391 if (!this.data.unique || !this.data.unique[objectId]) {
392 return;
393 }
394
395 var unique = this.data.unique[objectId];
396 if (unique.type != 'select') {
397 return;
398 }
399
400 var recordObj = document.getElementsByName('data[' + unique.table + '][' + recordUid + '][' + unique.field + ']');
401 var values = this.getValuesFromHashMap(unique.used);
402 if (recordObj.length) {
403 if (recordObj[0].hasOwnProperty('options')) {
404 var selectedValue = recordObj[0].options[recordObj[0].selectedIndex].value;
405 for (var i = 0; i < values.length; i++) {
406 if (values[i] != selectedValue) {
407 var $recordObject = $(recordObj[0]);
408 this.removeSelectOption($recordObject, values[i]);
409 }
410 }
411 }
412 }
413 },
414 // this function is applied to a newly inserted record by AJAX
415 // it removes the used select items, that should be unique
416 setUnique: function (objectId, recordUid, selectedValue) {
417 if (!this.data.unique || !this.data.unique[objectId]) {
418 return;
419 }
420 var $selector = $('#' + this.escapeObjectId(objectId) + '_selector');
421
422 var unique = this.data.unique[objectId];
423 if (unique.type == 'select') {
424 if (!(unique.selector && unique.max == -1)) {
425 var formName = 'data' + this.parseObjectId('parts', objectId, 3, 1, true);
426 var formObj = document.getElementsByName(formName);
427 var recordObj = document.getElementsByName('data[' + unique.table + '][' + recordUid + '][' + unique.field + ']');
428 var values = this.getValuesFromHashMap(unique.used);
429 if ($selector.length) {
430 // remove all items from the new select-item which are already used in other children
431 if (recordObj.length) {
432 var $recordObject = $(recordObj[0]);
433 for (var i = 0; i < values.length; i++) {
434 this.removeSelectOption($recordObject, values[i]);
435 }
436 // set the selected item automatically to the first of the remaining items if no selector is used
437 if (!unique.selector) {
438 selectedValue = recordObj[0].options[0].value;
439 recordObj[0].options[0].selected = true;
440 this.updateUnique(recordObj[0], objectId, formName, recordUid);
441 this.handleChangedField(recordObj[0], objectId + '[' + recordUid + ']');
442 }
443 }
444 for (var i = 0; i < values.length; i++) {
445 this.removeSelectOption($selector, values[i]);
446 }
447 if (typeof this.data.unique[objectId].used.length != 'undefined') {
448 this.data.unique[objectId].used = {};
449 }
450 this.data.unique[objectId].used[recordUid] = selectedValue;
451 }
452 // remove the newly used item from each select-field of the child records
453 if (formObj.length && selectedValue) {
454 var records = this.trimExplode(',', formObj[0].value);
455 for (var i = 0; i < records.length; i++) {
456 recordObj = document.getElementsByName('data[' + unique.table + '][' + records[i] + '][' + unique.field + ']');
457 if (recordObj.length && records[i] != recordUid) {
458 var $recordObject = $(recordObj[0]);
459 this.removeSelectOption($recordObject, selectedValue);
460 }
461 }
462 }
463 }
464 } else if (unique.type == 'groupdb') {
465 // add the new record to the used items:
466 this.data.unique[objectId].used[recordUid] = {'table': unique.elTable, 'uid': selectedValue};
467 }
468
469 // remove used items from a selector-box
470 if (unique.selector == 'select' && selectedValue) {
471 this.removeSelectOption($selector, selectedValue);
472 this.data.unique[objectId]['used'][recordUid] = selectedValue;
473 }
474 },
475
476 domAddNewRecord: function (method, insertObjectId, objectPrefix, htmlData) {
477 var $insertObject = $('#' + this.escapeObjectId(insertObjectId));
478 if (this.isBelowMax(objectPrefix)) {
479 if (method == 'bottom') {
480 $insertObject.append(htmlData);
481 } else if (method == 'after') {
482 $insertObject.after(htmlData);
483 }
484 } else {
485 var message = TYPO3.lang['FormEngine.maxItemsAllowed'].replace('{0}', this.data.config[objectPrefix].max);
486 var title = $insertObject.data('title');
487 top.TYPO3.Notification.error(title, message);
488 }
489 },
490
491 domAddRecordDetails: function (objectId, objectPrefix, expandSingle, htmlData) {
492 var hiddenValue, formObj, valueObj;
493 var escapeObjectId = this.escapeObjectId(objectId);
494 var $objectDiv = $('#' + escapeObjectId + '_fields');
495 if ($objectDiv.length == 0 || $objectDiv.html().substr(0, 16) !== '<!--notloaded-->') {
496 return;
497 }
498
499 var elName = this.parseObjectId('full', objectId, 2, 0, true);
500
501 var $formObj = $('[data-formengine-input-name="' + elName + '[hidden]"]');
502 var $valueObj = $('[name="' + elName + '[hidden]"]');
503
504 // It might be the case that a child record
505 // cannot be hidden at all (no hidden field)
506 if ($formObj.length && $valueObj.length) {
507 hiddenValue = $formObj[0].checked;
508 $formObj.first().remove();
509 $valueObj.first().remove();
510 }
511
512 // Update DOM
513 $objectDiv.html(htmlData);
514
515 formObj = document.querySelector('[data-formengine-input-name="' + elName + '[hidden]"]');
516 valueObj = document.getElementsByName(elName + '[hidden]');
517
518 // Set the hidden value again
519 if (typeof formObj !== 'undefined' && formObj !== null && valueObj.length) {
520 valueObj[0].value = hiddenValue ? 1 : 0;
521 formObj.checked = hiddenValue;
522 }
523
524 // now that the content is loaded, set the expandState
525 this.expandCollapseRecord(objectId, expandSingle);
526 },
527
528 changeSorting: function (objectId, direction) {
529 var objectName = 'data' + this.parseObjectId('parts', objectId, 3, 2, true);
530 var objectPrefix = this.parseObjectId('full', objectId, 0, 1);
531 var formObj = document.getElementsByName(objectName);
532
533 if (!formObj.length) {
534 return false;
535 }
536
537 // the uid of the calling object (last part in objectId)
538 var callingUid = this.parseObjectId('none', objectId, 1);
539 var records = this.trimExplode(',', formObj[0].value);
540 var current = records.indexOf(callingUid);
541 var changed = false;
542
543 // move up
544 if (direction > 0 && current > 0) {
545 records[current] = records[current - 1];
546 records[current - 1] = callingUid;
547 changed = true;
548
549 // move down
550 } else if (direction < 0 && current < records.length - 1) {
551 records[current] = records[current + 1];
552 records[current + 1] = callingUid;
553 changed = true;
554 }
555
556 if (changed) {
557 formObj[0].value = records.join(',');
558 var cAdj = direction > 0 ? 1 : 0; // adjustment
559 var objectIdPrefix = '#' + this.escapeObjectId(objectPrefix) + this.structureSeparator;
560 $(objectIdPrefix + records[current - cAdj] + '_div').insertBefore(
561 $(objectIdPrefix + records[current + 1 - cAdj] + '_div')
562 );
563 this.redrawSortingButtons(objectPrefix, records);
564 }
565
566 return false;
567 },
568
569 dragAndDropSorting: function (element) {
570 var objectId = element.getAttribute('id').replace(/_records$/, '');
571 var objectName = 'data' + inline.parseObjectId('parts', objectId, 3, 0, true);
572 var formObj = document.getElementsByName(objectName);
573 var $element = $(element);
574
575 if (!formObj.length) {
576 return;
577 }
578
579 var checked = [];
580 var order = [];
581 $element.find('.sortableHandle').each(function (i, e) {
582 order.push($(e).data('id').toString());
583 });
584 var records = this.trimExplode(',', formObj[0].value);
585
586 // check if ordered uid is really part of the records
587 // virtually deleted items might still be there but ordering shouldn't saved at all on them
588 for (var i = 0; i < order.length; i++) {
589 if (records.indexOf(order[i]) != -1) {
590 checked.push(order[i]);
591 }
592 }
593
594 formObj[0].value = checked.join(',');
595
596 if (inline.data.config && inline.data.config[objectId]) {
597 var table = inline.data.config[objectId].table;
598 inline.redrawSortingButtons(objectId + inline.structureSeparator + table, checked);
599 }
600 },
601
602 createDragAndDropSorting: function (objectId) {
603 require(['jquery', 'jquery-ui/sortable'], function ($) {
604 var $sortingContainer = $('#' + inline.escapeObjectId(objectId));
605
606 if ($sortingContainer.hasClass('ui-sortable')) {
607 $sortingContainer.sortable('enable');
608 return;
609 }
610
611 $sortingContainer.sortable({
612 containment: 'parent',
613 handle: '.sortableHandle',
614 zIndex: '4000',
615 axis: 'y',
616 tolerance: 'pointer',
617 stop: function () {
618 inline.dragAndDropSorting($sortingContainer[0]);
619 }
620 });
621 });
622 },
623
624 destroyDragAndDropSorting: function (objectId) {
625 require(['jquery', 'jquery-ui/sortable'], function ($) {
626 var $sortingContainer = $('#' + inline.escapeObjectId(objectId));
627 if (!$sortingContainer.hasClass('ui-sortable')) {
628 return;
629 }
630 $sortingContainer.sortable('disable');
631 });
632 },
633
634 redrawSortingButtons: function (objectPrefix, records) {
635 var i, $headerObj, sortUp, sortDown, partOfHeaderObj, iconIdentifier;
636
637 // if no records were passed, fetch them from form field
638 if (typeof records == 'undefined') {
639 records = [];
640 var objectName = 'data' + this.parseObjectId('parts', objectPrefix, 3, 1, true);
641 var formObj = document.getElementsByName(objectName);
642 if (formObj.length) {
643 records = this.trimExplode(',', formObj[0].value);
644 }
645 }
646 partOfHeaderObj = this.escapeObjectId(objectPrefix) + this.structureSeparator;
647 require(['TYPO3/CMS/Backend/Icons'], function(Icons) {
648 for (i = 0; i < records.length; i++) {
649 if (!records[i].length) {
650 continue;
651 }
652 $headerObj = TYPO3.jQuery('#' + partOfHeaderObj + records[i] + '_header');
653 sortUp = $headerObj.find('.sortingUp');
654 iconIdentifier = 'actions-move-up';
655 if (sortUp) {
656 if (i == 0) {
657 sortUp.addClass('disabled');
658 iconIdentifier = 'empty-empty';
659 } else {
660 sortUp.removeClass('disabled');
661 }
662 Icons.getIcon(iconIdentifier, Icons.sizes.small).done(function(markup) {
663 sortUp.find('.t3js-icon').replaceWith(markup);
664 });
665 }
666 sortDown = $headerObj.find('.sortingDown');
667 iconIdentifier = 'actions-move-down';
668 if (sortDown) {
669 if (i == records.length - 1) {
670 sortDown.addClass('disabled');
671 iconIdentifier = 'empty-empty';
672 } else {
673 sortDown.removeClass('disabled');
674 }
675 Icons.getIcon(iconIdentifier, Icons.sizes.small).done(function(markup) {
676 sortDown.find('.t3js-icon').replaceWith(markup);
677 });
678 }
679 }
680 });
681 },
682
683 memorizeAddRecord: function (objectPrefix, newUid, afterUid, selectedValue) {
684 if (this.isBelowMax(objectPrefix)) {
685 var objectName = 'data' + this.parseObjectId('parts', objectPrefix, 3, 1, true);
686 var formObj = document.getElementsByName(objectName);
687
688 if (formObj.length) {
689 var records = [];
690 if (formObj[0].value.length) {
691 records = this.trimExplode(',', formObj[0].value);
692 }
693
694 if (afterUid) {
695 var newRecords = [];
696 for (var i = 0; i < records.length; i++) {
697 if (records[i].length) {
698 newRecords.push(records[i]);
699 }
700 if (afterUid == records[i]) {
701 newRecords.push(newUid);
702 }
703 }
704 records = newRecords;
705 } else {
706 records.push(newUid);
707 }
708 formObj[0].value = records.join(',');
709 }
710
711 this.redrawSortingButtons(objectPrefix, records);
712
713 if (this.data.unique && this.data.unique[objectPrefix]) {
714 this.setUnique(objectPrefix, newUid, selectedValue);
715 }
716 }
717
718 // if we reached the maximum of possible records after this action, hide the new buttons
719 if (!this.isBelowMax(objectPrefix)) {
720 var objectParent = this.parseObjectId('full', objectPrefix, 0, 1);
721 var md5 = this.getObjectMD5(objectParent);
722 this.hideElementsWithClassName('.inlineNewButton' + (md5 ? '.' + md5 : ''), objectParent);
723 this.hideElementsWithClassName('.inlineNewRelationButton' + (md5 ? '.' + md5 : ''), objectParent);
724 this.hideElementsWithClassName('.inlineNewFileUploadButton' + (md5 ? '.' + md5 : ''), objectParent);
725 this.hideElementsWithClassName('.t3js-online-media-add-btn' + (md5 ? '.' + md5 : ''), objectParent);
726 this.hideElementsWithClassName('.inlineForeignSelector' + (md5 ? '.' + md5 : ''), 't3-form-field-item');
727 }
728
729 if (TBE_EDITOR) {
730 TBE_EDITOR.fieldChanged_fName(objectName, formObj);
731 }
732 },
733
734 memorizeRemoveRecord: function (objectName, removeUid) {
735 var formObj = document.getElementsByName(objectName);
736 if (formObj.length) {
737 var parts = [],
738 indexOfRemoveUid = -1;
739 if (formObj[0].value.length) {
740 parts = this.trimExplode(',', formObj[0].value);
741 indexOfRemoveUid = parts.indexOf(removeUid);
742 if (indexOfRemoveUid !== -1) {
743 delete parts[indexOfRemoveUid];
744 }
745 formObj[0].value = parts.join(',');
746 if (TBE_EDITOR) {
747 TBE_EDITOR.fieldChanged_fName(objectName, formObj);
748 }
749 return parts.length;
750 }
751 }
752 return false;
753 },
754
755 updateUnique: function (srcElement, objectPrefix, formName, recordUid) {
756 if (!this.data.unique || !this.data.unique[objectPrefix]) {
757 return;
758 }
759
760 var unique = this.data.unique[objectPrefix];
761 var oldValue = unique.used[recordUid];
762
763 if (unique.selector == 'select') {
764 var selector = $(objectPrefix + '_selector');
765 this.removeSelectOption(selector, srcElement.value);
766 if (typeof oldValue != 'undefined') {
767 this.readdSelectOption(selector, oldValue, unique);
768 }
769 }
770
771 if (unique.selector && unique.max == -1) {
772 return;
773 }
774
775 var formObj = document.getElementsByName(formName);
776 if (!unique || !formObj.length) {
777 return;
778 }
779
780 var records = this.trimExplode(',', formObj[0].value);
781 var recordObj;
782 for (var i = 0; i < records.length; i++) {
783 recordObj = document.getElementsByName('data[' + unique.table + '][' + records[i] + '][' + unique.field + ']');
784 if (recordObj.length && recordObj[0] != srcElement) {
785 var $recordObject = $(recordObj[0]);
786 this.removeSelectOption($recordObject, srcElement.value);
787 if (typeof oldValue != 'undefined') {
788 this.readdSelectOption($recordObject, oldValue, unique);
789 }
790 }
791 }
792 this.data.unique[objectPrefix].used[recordUid] = srcElement.value;
793 },
794
795 revertUnique: function (objectPrefix, elName, recordUid) {
796 if (!this.data.unique || !this.data.unique[objectPrefix]) {
797 return;
798 }
799
800 var unique = this.data.unique[objectPrefix];
801 var fieldObj = elName ? document.getElementsByName(elName + '[' + unique.field + ']') : null;
802
803 if (unique.type == 'select') {
804 if (!fieldObj || !fieldObj.length) {
805 return;
806 }
807
808 delete(this.data.unique[objectPrefix].used[recordUid]);
809
810 if (unique.selector == 'select') {
811 if (!isNaN(fieldObj[0].value)) {
812 var $selector = $('#' + this.escapeObjectId(objectPrefix) + '_selector');
813 this.readdSelectOption($selector, fieldObj[0].value, unique);
814 }
815 }
816
817 if (unique.selector && unique.max == -1) {
818 return;
819 }
820
821 var formName = 'data' + this.parseObjectId('parts', objectPrefix, 3, 1, true);
822 var formObj = document.getElementsByName(formName);
823 if (!formObj.length) {
824 return;
825 }
826
827 var records = this.trimExplode(',', formObj[0].value);
828 var recordObj;
829 // walk through all inline records on that level and get the select field
830 for (var i = 0; i < records.length; i++) {
831 recordObj = document.getElementsByName('data[' + unique.table + '][' + records[i] + '][' + unique.field + ']');
832 if (recordObj.length) {
833 var $recordObject = $(recordObj[0]);
834 this.readdSelectOption($recordObject, fieldObj[0].value, unique);
835 }
836 }
837 } else if (unique.type == 'groupdb') {
838 delete(this.data.unique[objectPrefix].used[recordUid])
839 }
840 },
841
842 enableDisableRecord: function (objectIdentifier, fieldName) {
843 var elName = this.parseObjectId('full', objectIdentifier, 2, 0, true) + '[' + fieldName + ']';
844 var formObj = document.querySelector('[data-formengine-input-name="' + elName + '"]');
845 var valueObj = document.getElementsByName(elName);
846 var escapedObjectIdentifier = this.escapeObjectId(objectIdentifier);
847 var $container = $('#' + escapedObjectIdentifier + '_div');
848 var $icon = $container.find('.t3js-' + escapedObjectIdentifier + '_disabled .t3js-icon');
849
850 // It might be the case that there's no hidden field
851 if (typeof formObj !== 'undefined' && formObj !== null && valueObj.length) {
852 formObj.click();
853 valueObj[0].value = formObj.checked ? 1 : 0;
854 TBE_EDITOR.fieldChanged_fName(elName, elName);
855 }
856
857 if ($icon.length) {
858 require(['TYPO3/CMS/Backend/Icons'], function(Icons) {
859 var hiddenClass = 't3-form-field-container-inline-hidden',
860 isHidden = $container.hasClass(hiddenClass),
861 toggleIcon;
862
863 if (isHidden) {
864 toggleIcon = 'actions-edit-hide';
865 $container.removeClass(hiddenClass);
866 } else {
867 toggleIcon = 'actions-edit-unhide';
868 $container.addClass(hiddenClass);
869 }
870
871 Icons.getIcon(toggleIcon, Icons.sizes.small).done(function(markup) {
872 $icon.replaceWith(markup);
873 });
874 });
875 }
876
877 return false;
878 },
879
880 deleteRecord: function (objectId, options) {
881 var i, j, inlineRecords, records, childObjectId, childTable;
882 var objectPrefix = this.parseObjectId('full', objectId, 0, 1);
883 var elName = this.parseObjectId('full', objectId, 2, 0, true);
884 var shortName = this.parseObjectId('parts', objectId, 2, 0, true);
885 var recordUid = this.parseObjectId('none', objectId, 1);
886 var beforeDeleteIsBelowMax = this.isBelowMax(objectPrefix);
887
888 // revert the unique settings if available
889 this.revertUnique(objectPrefix, elName, recordUid);
890
891 // Remove from TBE_EDITOR (required fields, required range, etc.):
892 if (TBE_EDITOR && TBE_EDITOR.removeElement) {
893 var removeStack = [];
894 // Iterate over all child records:
895 inlineRecords = $('.inlineRecord', '#' + objectId + '_div');
896 // Remove nested child records from TBE_EDITOR required/range checks:
897 for (i = inlineRecords.length - 1; i >= 0; i--) {
898 if (inlineRecords.get(i).value.length) {
899 records = this.trimExplode(',', inlineRecords.get(i).value);
900 childObjectId = this.data.map[inlineRecords.get(i).name];
901 childTable = this.data.config[childObjectId].table;
902 for (j = records.length - 1; j >= 0; j--) {
903 removeStack.push('data[' + childTable + '][' + records[j] + ']');
904 }
905 }
906 }
907 removeStack.push('data' + shortName);
908 TBE_EDITOR.removeElementArray(removeStack);
909 }
910
911 // Mark this container as deleted
912 $('#' + this.escapeObjectId(objectId) + '_div')
913 .addClass('inlineIsDeletedRecord')
914 .addClass('t3js-inline-record-deleted');
915
916 // If the record is new and was never saved before, just remove it from DOM:
917 if (this.isNewRecord(objectId) || options && options.forceDirectRemoval) {
918 this.fadeAndRemove(objectId + '_div');
919 // If the record already exists in storage, mark it to be deleted on clicking the save button:
920 } else {
921 document.getElementsByName('cmd' + shortName + '[delete]')[0].disabled = false;
922 $('#' + this.escapeObjectId(objectId) + '_div').fadeOut(200);
923 }
924
925 var recordCount = this.memorizeRemoveRecord(
926 'data' + this.parseObjectId('parts', objectId, 3, 2, true),
927 recordUid
928 );
929
930 if (recordCount <= 1) {
931 this.destroyDragAndDropSorting(this.parseObjectId('full', objectId, 0, 2) + '_records');
932 }
933 this.redrawSortingButtons(objectPrefix);
934
935 // if the NEW-button was hidden and now we can add again new children, show the button
936 if (!beforeDeleteIsBelowMax && this.isBelowMax(objectPrefix)) {
937 var objectParent = this.parseObjectId('full', objectPrefix, 0, 1);
938 var md5 = this.getObjectMD5(objectParent);
939 this.showElementsWithClassName('.inlineNewButton' + (md5 ? '.' + md5 : ''), objectParent);
940 this.showElementsWithClassName('.inlineNewRelationButton' + (md5 ? '.' + md5 : ''), objectParent);
941 this.showElementsWithClassName('.inlineNewFileUploadButton' + (md5 ? '.' + md5 : ''), objectParent);
942 this.showElementsWithClassName('.t3js-online-media-add-btn' + (md5 ? '.' + md5 : ''), objectParent);
943 this.showElementsWithClassName('.inlineForeignSelector' + (md5 ? '.' + md5 : ''), 't3-form-field-item');
944 }
945 TYPO3.FormEngine.Validation.validate();
946 return false;
947 },
948
949 parseFormElementName: function (wrap, formElementName, rightCount, skipRight) {
950 var idParts = this.splitFormElementName(formElementName);
951
952 if (!wrap) {
953 wrap = 'full';
954 }
955 if (!skipRight) {
956 skipRight = 0;
957 }
958
959 var elParts = [];
960 for (var i = 0; i < skipRight; i++) {
961 idParts.pop();
962 }
963
964 if (rightCount > 0) {
965 for (var i = 0; i < rightCount; i++) {
966 elParts.unshift(idParts.pop());
967 }
968 } else {
969 for (var i = 0; i < -rightCount; i++) {
970 idParts.shift();
971 }
972 elParts = idParts;
973 }
974
975 return this.constructFormElementName(wrap, elParts);
976 },
977
978 splitFormElementName: function (formElementName) {
979 // remove left and right side "data[...|...]" -> '...|...'
980 formElementName = formElementName.substr(0, formElementName.lastIndexOf(']')).substr(formElementName.indexOf('[') + 1);
981 return formElementName.split('][');
982 },
983
984 splitObjectId: function (objectId) {
985 objectId = objectId.substr(objectId.indexOf(this.structureSeparator) + 1);
986 objectId = objectId.split(this.flexFormSeparator).join(this.flexFormSubstitute);
987 return objectId.split(this.structureSeparator);
988 },
989
990 constructFormElementName: function (wrap, parts) {
991 var elReturn;
992
993 if (wrap == 'full') {
994 elReturn = 'data[' + parts.join('][') + ']';
995 elReturn = elReturn.split(this.flexFormSubstitute).join('][');
996 } else if (wrap == 'parts') {
997 elReturn = '[' + parts.join('][') + ']';
998 elReturn = elReturn.split(this.flexFormSubstitute).join('][');
999 } else if (wrap == 'none') {
1000 elReturn = parts.length > 1 ? parts : parts.join('');
1001 }
1002
1003 return elReturn;
1004 },
1005
1006 constructObjectId: function (wrap, parts) {
1007 var elReturn;
1008
1009 if (wrap == 'full') {
1010 elReturn = 'data' + this.structureSeparator + parts.join(this.structureSeparator);
1011 elReturn = elReturn.split(this.flexFormSubstitute).join(this.flexFormSeparator);
1012 } else if (wrap == 'parts') {
1013 elReturn = this.structureSeparator + parts.join(this.structureSeparator);
1014 elReturn = elReturn.split(this.flexFormSubstitute).join(this.flexFormSeparator);
1015 } else if (wrap == 'none') {
1016 elReturn = parts.length > 1 ? parts : parts.join('');
1017 }
1018
1019 return elReturn;
1020 },
1021
1022 parseObjectId: function (wrap, objectId, rightCount, skipRight, returnAsFormElementName) {
1023 var idParts = this.splitObjectId(objectId);
1024
1025 if (!wrap) {
1026 wrap = 'full';
1027 }
1028 if (!skipRight) {
1029 skipRight = 0;
1030 }
1031
1032 var elParts = [];
1033 for (var i = 0; i < skipRight; i++) {
1034 idParts.pop();
1035 }
1036
1037 if (rightCount > 0) {
1038 for (var i = 0; i < rightCount; i++) {
1039 elParts.unshift(idParts.pop());
1040 }
1041 } else {
1042 for (var i = 0; i < -rightCount; i++) {
1043 idParts.shift();
1044 }
1045 elParts = idParts;
1046 }
1047
1048 return returnAsFormElementName
1049 ? this.constructFormElementName(wrap, elParts)
1050 : this.constructObjectId(wrap, elParts);
1051 },
1052
1053 handleChangedField: function (formField, objectId) {
1054 var formObj;
1055 if (typeof formField == 'object') {
1056 formObj = formField;
1057 } else {
1058 formObj = document.getElementsByName(formField);
1059 if (formObj.length) {
1060 formObj = formObj[0];
1061 }
1062 }
1063
1064 if (formObj != undefined) {
1065 var value;
1066 if (formObj.nodeName == 'SELECT') {
1067 value = formObj.options[formObj.selectedIndex].text;
1068 } else {
1069 value = formObj.value;
1070 }
1071 $('#' + this.escapeObjectId(objectId) + '_label').text(value.length ? value : this.noTitleString);
1072 }
1073 return true;
1074 },
1075
1076 arrayAssocCount: function (object) {
1077 var count = 0;
1078 if (typeof object.length != 'undefined') {
1079 count = object.length;
1080 } else {
1081 for (var i in object) {
1082 count++;
1083 }
1084 }
1085 return count;
1086 },
1087
1088 isBelowMax: function (objectPrefix) {
1089 var isBelowMax = true;
1090 var objectName = 'data' + this.parseObjectId('parts', objectPrefix, 3, 1, true);
1091 var formObj = document.getElementsByName(objectName);
1092
1093 if (this.data.config && this.data.config[objectPrefix] && formObj.length) {
1094 var recordCount = formObj[0].value ? this.trimExplode(',', formObj[0].value).length : 0;
1095 if (recordCount >= this.data.config[objectPrefix].max) {
1096 isBelowMax = false;
1097 }
1098 }
1099 if (isBelowMax && this.data.unique && this.data.unique[objectPrefix]) {
1100 var unique = this.data.unique[objectPrefix];
1101 if (this.arrayAssocCount(unique.used) >= unique.max && unique.max >= 0) {
1102 isBelowMax = false;
1103 }
1104 }
1105 return isBelowMax;
1106 },
1107
1108 getOptionsHash: function ($selectObj) {
1109 var optionsHash = {};
1110 $selectObj.find('option').each(function (i, option) {
1111 optionsHash[option.value] = i;
1112 });
1113 return optionsHash;
1114 },
1115
1116 removeSelectOption: function ($selectObj, value) {
1117 var optionsHash = this.getOptionsHash($selectObj);
1118 if (optionsHash[value] != undefined) {
1119 $selectObj.find('option').eq(optionsHash[value]).remove();
1120 }
1121 },
1122
1123 readdSelectOption: function ($selectObj, value, unique) {
1124 if (!$selectObj.length) {
1125 return;
1126 }
1127
1128 var index = null;
1129 var optionsHash = this.getOptionsHash($selectObj);
1130
1131 for (var possibleValue in unique.possible) {
1132 if (possibleValue == value) {
1133 break;
1134 }
1135 if (optionsHash[possibleValue] != undefined) {
1136 index = optionsHash[possibleValue];
1137 }
1138 }
1139
1140 if (index == null) {
1141 index = 0;
1142 } else if (index < $selectObj.find('option').length) {
1143 index++;
1144 }
1145 // recreate the <option> tag
1146 var readdOption = document.createElement('option');
1147 readdOption.text = unique.possible[value];
1148 readdOption.value = value;
1149 // add the <option> at the right position
1150 // I didn't find a possibility to add an option to a predefined position
1151 // with help of an index in jQuery. So we realized it the "old" style
1152 var selectObj = $selectObj.get(0);
1153 selectObj.add(readdOption, document.all ? index : selectObj.options[index]);
1154 },
1155
1156 hideElementsWithClassName: function (selector, parentElement) {
1157 $('#' + this.escapeObjectId(parentElement)).find(selector).fadeOut(200);
1158 },
1159
1160 showElementsWithClassName: function (selector, parentElement) {
1161 $('#' + this.escapeObjectId(parentElement)).find(selector).fadeIn(200);
1162 },
1163
1164 // sets the opacity to 0.2 and then fades in to opacity 1
1165 fadeOutFadeIn: function (objectId) {
1166 $('#' + this.escapeObjectId(objectId)).css({opacity: 0.2}).fadeTo(200, 1, 'linear');
1167 },
1168
1169 isNewRecord: function (objectId) {
1170 var $selector = $('#' + this.escapeObjectId(objectId) + '_div');
1171 return $selector.length && $selector.hasClass('inlineIsNewRecord');
1172 },
1173
1174 // Find and fix nested of inline and tab levels if a new element was created dynamically (it doesn't know about its nesting):
1175 findContinuedNestedLevel: function (nested, objectId) {
1176 if (this.data.nested && this.data.nested[objectId]) {
1177 // Remove the first element from the new nested stack, it's just a hint:
1178 nested.shift();
1179 nested = this.data.nested[objectId].concat(nested);
1180 }
1181 return nested;
1182 },
1183
1184 getObjectMD5: function (objectPrefix) {
1185 var md5 = false;
1186 if (this.data.config && this.data.config[objectPrefix] && this.data.config[objectPrefix].md5) {
1187 md5 = this.data.config[objectPrefix].md5;
1188 }
1189 return md5
1190 },
1191
1192 fadeAndRemove: function (element) {
1193 $('#' + this.escapeObjectId(element)).fadeOut(200, function () {
1194 $(this).remove();
1195 TYPO3.FormEngine.Validation.validate();
1196 });
1197 },
1198
1199 getContext: function (objectId) {
1200 var result = null;
1201
1202 if (objectId !== '' && typeof this.data.config[objectId] !== 'undefined' && typeof this.data.config[objectId].context !== 'undefined') {
1203 result = this.data.config[objectId].context;
1204 }
1205
1206 return result;
1207 },
1208
1209 /**
1210 * Escapes object identifiers to be used in jQuery.
1211 *
1212 * @param {String} objectId
1213 * @return string
1214 */
1215 escapeObjectId: function (objectId) {
1216 var escapedObjectId;
1217 escapedObjectId = objectId.replace(/:/g, '\\:');
1218 escapedObjectId = escapedObjectId.replace(/\./g, '\\.');
1219 return escapedObjectId;
1220 },
1221
1222 /**
1223 * Escapes object identifiers to be used as jQuery selector.
1224 *
1225 * @param {String} objectId
1226 * @return string
1227 * @deprecated since TYPO3 CMS v8, this method will be removed in TYPO3 CMS v9. Use $.escapeSelector() instead, which was added with jQuery 3.0.
1228 */
1229 escapeSelectorObjectId: function (objectId) {
1230 return $.escapeSelector(objectId);
1231 },
1232
1233 /**
1234 * Helper function to get clean trimmed array from comma list
1235 *
1236 * @param {String} delimiter
1237 * @param {String} string
1238 * @returns {Array}
1239 */
1240 trimExplode: function(delimiter, string) {
1241 var result = [];
1242 var items = string.split(delimiter);
1243 for (var i=0; i<items.length; i++) {
1244 var item = items[i].trim();
1245 if (item.length > 0) {
1246 result.push(item);
1247 }
1248 }
1249 return result;
1250 }
1251 };
1252
1253 /*]]>*/
1254 $(function () {
1255 $(document).on('click', '[data-toggle="formengine-inline"]', function(event) {
1256 inline.toggleEvent(event);
1257 });
1258 });