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