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