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