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