6c0374f301a496bca82bb2a9122ff46f8ae0b3ee
[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 prependFormFieldNames: 'data',
27 noTitleString: '[No title]',
28 lockedAjaxMethod: {},
29 sourcesLoaded: {},
30 data: {},
31 isLoading: false,
32
33 addToDataArray: function (object) {
34 TYPO3.jQuery.each(object, function (key, value) {
35 if (!inline.data[key]) {
36 inline.data[key] = {};
37 }
38 TYPO3.jQuery.extend(inline.data[key], value);
39 });
40 },
41 setPrependFormFieldNames: function (value) {
42 this.prependFormFieldNames = value;
43 },
44 setNoTitleString: function (value) {
45 this.noTitleString = value;
46 },
47 toggleEvent: function (event) {
48 var $triggerElement = TYPO3.jQuery(event.target);
49 if ($triggerElement.parents('.t3js-formengine-irre-control').length == 1) {
50 return;
51 }
52
53 var $recordHeader = TYPO3.jQuery(this);
54 inline.expandCollapseRecord(
55 $recordHeader.attr('id').replace('_header', ''),
56 $recordHeader.attr('data-expandSingle'),
57 $recordHeader.attr('data-returnURL')
58 );
59 },
60 expandCollapseRecord: function (objectId, expandSingle, returnURL) {
61 var currentUid = this.parseObjectId('none', objectId, 1);
62 var objectPrefix = this.parseObjectId('full', objectId, 0, 1);
63 var escapedObjectId = this.escapeObjectId(objectId);
64
65 var $currentObject = TYPO3.jQuery('#' + escapedObjectId + '_div');
66 // if content is not loaded yet, get it now from server
67 if (inline.isLoading) {
68 return false;
69 } else if (TYPO3.jQuery('#' + escapedObjectId + '_fields').length > 0 && TYPO3.jQuery('#' + escapedObjectId + '_fields').html().substr(0, 16) == '<!--notloaded-->') {
70 inline.isLoading = true;
71 var headerIdentifier = '#' + escapedObjectId + '_header';
72 // add loading-indicator
73 require(['nprogress'], function(NProgress) {
74 inline.progress = NProgress;
75 inline.progress.configure({parent: headerIdentifier, showSpinner: false});
76 inline.progress.start();
77 });
78 return this.getRecordDetails(objectId, returnURL);
79 }
80
81 var isCollapsed = $currentObject.hasClass(this.classCollapsed);
82 var collapse = [];
83 var expand = [];
84
85 // if only a single record should be visibly for that set of records
86 // and the record clicked itself is no visible, collapse all others
87 if (expandSingle && $currentObject.hasClass(this.classCollapsed)) {
88 collapse = this.collapseAllRecords(objectId, objectPrefix, currentUid);
89 }
90
91 inline.toggleElement(objectId);
92
93 if (this.isNewRecord(objectId)) {
94 this.updateExpandedCollapsedStateLocally(objectId, isCollapsed);
95 } else if (isCollapsed) {
96 expand.push(currentUid);
97 } else if (!isCollapsed) {
98 collapse.push(currentUid);
99 }
100
101 this.setExpandedCollapsedState(objectId, expand.join(','), collapse.join(','));
102
103 return false;
104 },
105
106 toggleElement: function (objectId) {
107 var escapedObjectId = this.escapeObjectId(objectId);
108 var $jQueryObject = TYPO3.jQuery('#' + escapedObjectId + '_div');
109
110 if ($jQueryObject.hasClass(this.classCollapsed)) {
111 $jQueryObject.removeClass(this.classCollapsed).addClass(this.classVisible);
112 $jQueryObject.find('#' + escapedObjectId + '_header .t3-icon-irre-collapsed').removeClass('t3-icon-irre-collapsed').addClass('t3-icon-irre-expanded');
113 } else {
114 $jQueryObject.removeClass(this.classVisible).addClass(this.classCollapsed);
115 $jQueryObject.find('#' + escapedObjectId + '_header .t3-icon-irre-expanded').addClass('t3-icon-irre-collapsed').removeClass('t3-icon-irre-expanded');
116 }
117 },
118 collapseAllRecords: function (objectId, objectPrefix, callingUid) {
119 // get the form field, where all records are stored
120 var objectName = this.prependFormFieldNames + this.parseObjectId('parts', objectId, 3, 2, true);
121 var formObj = document.getElementsByName(objectName);
122 var collapse = [];
123
124 if (formObj.length) {
125 // the uid of the calling object (last part in objectId)
126 var recObjectId = '', escapedRecordObjectId;
127
128 var records = formObj[0].value.split(',');
129 for (var i = 0; i < records.length; i++) {
130 recObjectId = objectPrefix + this.structureSeparator + records[i];
131 escapedRecordObjectId = this.escapeObjectId(recObjectId);
132
133 var $recordEntry = TYPO3.jQuery('#' + escapedRecordObjectId);
134 if (records[i] != callingUid && $recordEntry.hasClass(this.classVisible)) {
135 TYPO3.jQuery('#' + escapedRecordObjectId + '_div').removeClass(this.classVisible).addClass(this.classCollapsed);
136 if (this.isNewRecord(recObjectId)) {
137 this.updateExpandedCollapsedStateLocally(recObjectId, 0);
138 } else {
139 collapse.push(records[i]);
140 }
141 }
142 }
143 }
144
145 return collapse;
146 },
147
148 updateExpandedCollapsedStateLocally: function (objectId, value) {
149 var ucName = 'uc[inlineView]' + this.parseObjectId('parts', objectId, 3, 2, true);
150 var ucFormObj = document.getElementsByName(ucName);
151 if (ucFormObj.length) {
152 ucFormObj[0].value = value;
153 }
154 },
155
156 getRecordDetails: function (objectId, returnURL) {
157 var context = this.getContext(this.parseObjectId('full', objectId, 0, 1));
158 inline.makeAjaxCall('getRecordDetails', [objectId, returnURL], true, context);
159 return false;
160 },
161
162 createNewRecord: function (objectId, recordUid) {
163 if (this.isBelowMax(objectId)) {
164 var context = this.getContext(objectId);
165 if (recordUid) {
166 objectId += this.structureSeparator + recordUid;
167 }
168 this.makeAjaxCall('createNewRecord', [objectId], true, context);
169 } else {
170 var message = TBE_EDITOR.labels.maxItemsAllowed.replace('{0}', this.data.config[objectId].max);
171 var matches = objectId.match(/^(data-\d+-.*?-\d+-.*?)-(.*?)$/);
172 var title = '';
173 if (matches) {
174 title = TYPO3.jQuery('#' + matches[1] + '_records').data('title');
175 }
176 top.TYPO3.Notification.error(title, message, 5);
177 }
178 return false;
179 },
180
181 synchronizeLocalizeRecords: function (objectId, type) {
182 var context = this.getContext(objectId);
183 var parameters = [objectId, type];
184 this.makeAjaxCall('synchronizeLocalizeRecords', parameters, true, context);
185 },
186
187 setExpandedCollapsedState: function (objectId, expand, collapse) {
188 var context = this.getContext(objectId);
189 this.makeAjaxCall('setExpandedCollapsedState', [objectId, expand, collapse], false, context);
190 },
191
192 makeAjaxCall: function (method, params, lock, context) {
193 var url = '', urlParams = '', options = {};
194 if (method && params && params.length && this.lockAjaxMethod(method, lock)) {
195 url = TYPO3.settings.ajaxUrls['t3lib_TCEforms_inline::' + method];
196 urlParams = '';
197 for (var i = 0, max = params.length; i < max; i++) {
198 urlParams += '&ajax[' + i + ']=' + encodeURIComponent(params[i]);
199 }
200 if (context) {
201 urlParams += '&ajax[context]=' + encodeURIComponent(Object.toJSON(context));
202 }
203 options = {
204 type: 'POST',
205 data: urlParams,
206 success: function (data, message, jqXHR) {
207 inline.isLoading = false;
208 inline.processAjaxResponse(method, jqXHR);
209 if (inline.progress) {
210 inline.progress.done();
211 }
212 },
213 error: function (jqXHR, statusText, errorThrown) {
214 inline.isLoading = false;
215 inline.showAjaxFailure(method, jqXHR);
216 if (inline.progress) {
217 inline.progress.done();
218 }
219 }
220 };
221
222 TYPO3.jQuery.ajax(url, options);
223 }
224 },
225
226 lockAjaxMethod: function (method, lock) {
227 if (!lock || !inline.lockedAjaxMethod[method]) {
228 inline.lockedAjaxMethod[method] = true;
229 return true;
230 } else {
231 return false;
232 }
233 },
234
235 unlockAjaxMethod: function (method) {
236 inline.lockedAjaxMethod[method] = false;
237 },
238
239 processAjaxResponse: function (method, xhr, json) {
240 var addTag = null, restart = false, processedCount = 0, element = null, errorCatch = [], sourcesWaiting = [];
241 if (!json && xhr) {
242 json = xhr.responseJSON;
243 }
244 // If there are elements the should be added to the <HEAD> tag (e.g. for RTEhtmlarea):
245 if (json.headData) {
246 var head = inline.getDomHeadTag();
247 var headTags = inline.getDomHeadChildren(head);
248 TYPO3.jQuery.each(json.headData, function (index, addTag) {
249 if (restart) {
250 return;
251 }
252 if (!addTag || (!addTag.innerHTML && inline.searchInDomTags(headTags, addTag))) {
253 return;
254 }
255
256 if (addTag.name == 'SCRIPT' && addTag.innerHTML && processedCount) {
257 restart = true;
258 return false;
259 } else {
260 if (addTag.name == 'SCRIPT' && addTag.innerHTML) {
261 try {
262 eval(addTag.innerHTML);
263 } catch (e) {
264 errorCatch.push(e);
265 }
266 } else {
267 element = inline.createNewDomElement(addTag);
268 // Set onload handler for external JS scripts:
269 if (addTag.name == 'SCRIPT' && element.src) {
270 element.onload = inline.sourceLoadedHandler(element);
271 sourcesWaiting.push(element.src);
272 }
273 head.appendChild(element);
274 processedCount++;
275 }
276 delete(json.headData[index]);
277 }
278 });
279 }
280 if (restart || processedCount) {
281 window.setTimeout(function () {
282 inline.reprocessAjaxResponse(method, json, sourcesWaiting);
283 }, 40);
284 } else {
285 if (method) {
286 inline.unlockAjaxMethod(method);
287 }
288 if (json.scriptCall && json.scriptCall.length > 0) {
289 TYPO3.jQuery.each(json.scriptCall, function (index, value) {
290 eval(value);
291 });
292 }
293 TYPO3.FormEngine.reinitialize();
294 }
295 },
296
297 // Check if dynamically added scripts are loaded and restart inline.processAjaxResponse():
298 reprocessAjaxResponse: function (method, json, sourcesWaiting) {
299 var sourcesLoaded = true;
300 if (sourcesWaiting && sourcesWaiting.length) {
301 TYPO3.jQuery.each(sourcesWaiting, function (index, source) {
302 if (!inline.sourcesLoaded[source]) {
303 sourcesLoaded = false;
304 return false;
305 }
306 });
307 }
308 if (sourcesLoaded) {
309 TYPO3.jQuery.each(sourcesWaiting, function (index, source) {
310 delete(inline.sourcesLoaded[source]);
311 });
312 window.setTimeout(function () {
313 inline.processAjaxResponse(method, null, json);
314 }, 80);
315 } else {
316 window.setTimeout(function () {
317 inline.reprocessAjaxResponse(method, json, sourcesWaiting);
318 }, 40);
319 }
320 },
321
322 sourceLoadedHandler: function (element) {
323 if (element && element.src) {
324 inline.sourcesLoaded[element.src] = true;
325 }
326 },
327
328 showAjaxFailure: function (method, xhr) {
329 inline.unlockAjaxMethod(method);
330 alert('Error: ' + xhr.status + "\n" + xhr.statusText);
331 },
332
333 // foreign_selector: used by selector box (type='select')
334 importNewRecord: function (objectId) {
335 var $selector = TYPO3.jQuery('#' + this.escapeObjectId(objectId) + '_selector');
336 var selectedIndex = $selector.prop('selectedIndex');
337 if (selectedIndex != -1) {
338 var context = this.getContext(objectId);
339 var selectedValue = $selector.val();
340 if (!this.data.unique || !this.data.unique[objectId]) {
341 $selector.find('option').eq(selectedIndex).prop('selected', false);
342 }
343 this.makeAjaxCall('createNewRecord', [objectId, selectedValue], true, context);
344 }
345 return false;
346 },
347
348 // foreign_selector: used by element browser (type='group/db')
349 importElement: function (objectId, table, uid, type) {
350 var context = this.getContext(objectId);
351 inline.makeAjaxCall('createNewRecord', [objectId, uid], true, context);
352 },
353
354 importElementMultiple: function (objectId, table, uidArray, type) {
355 TYPO3.jQuery.each(uidArray, function (index, uid) {
356 inline.delayedImportElement(objectId, table, uid, type);
357 });
358 },
359 delayedImportElement: function (objectId, table, uid, type) {
360 if (inline.lockedAjaxMethod['createNewRecord'] == true) {
361 window.setTimeout("inline.delayedImportElement('" + objectId + "','" + table + "'," + uid + ", null );",
362 300);
363 } else {
364 inline.importElement(objectId, table, uid, type);
365 }
366 },
367 // Check uniqueness for element browser:
368 checkUniqueElement: function (objectId, table, uid, type) {
369 if (this.checkUniqueUsed(objectId, uid, table)) {
370 return {passed: false, message: 'There is already a relation to the selected element!'};
371 } else {
372 return {passed: true};
373 }
374 },
375
376 // Checks if a record was used and should be unique:
377 checkUniqueUsed: function (objectId, uid, table) {
378 if (!this.data.unique || !this.data.unique[objectId]) {
379 return false;
380 }
381
382 var unique = this.data.unique[objectId];
383 var values = this.getValuesFromHashMap(unique.used);
384
385 // for select: only the uid is stored
386 if (unique['type'] == 'select') {
387 if (values.indexOf(uid) != -1) {
388 return true;
389 }
390
391 // for group/db: table and uid is stored in a assoc array
392 } else if (unique.type == 'groupdb') {
393 for (var i = values.length - 1; i >= 0; i--) {
394 // if the pair table:uid is already used:
395 if (values[i].table == table && values[i].uid == uid) {
396 return true;
397 }
398 }
399 }
400
401 return false;
402 },
403
404 setUniqueElement: function (objectId, table, uid, type, elName) {
405 var recordUid = this.parseFormElementName('none', elName, 1, 1);
406 // alert(objectId+'/'+table+'/'+uid+'/'+recordUid);
407 this.setUnique(objectId, recordUid, uid);
408 },
409
410 getKeysFromHashMap: function (unique) {
411 return TYPO3.jQuery.map(unique, function(value, key) {
412 return key;
413 });
414 },
415
416 getValuesFromHashMap: function (hashMap) {
417 return TYPO3.jQuery.map(hashMap, function(value, key) {
418 return value;
419 });
420 },
421
422 // Remove all select items already used
423 // from a newly retrieved/expanded record
424 removeUsed: function (objectId, recordUid) {
425 if (!this.data.unique || !this.data.unique[objectId]) {
426 return;
427 }
428
429 var unique = this.data.unique[objectId];
430 if (unique.type != 'select') {
431 return;
432 }
433
434 var formName = this.prependFormFieldNames + this.parseObjectId('parts', objectId, 3, 1, true);
435 var formObj = document.getElementsByName(formName);
436 var recordObj = document.getElementsByName(this.prependFormFieldNames + '[' + unique.table + '][' + recordUid + '][' + unique.field + ']');
437 var values = this.getValuesFromHashMap(unique.used);
438 if (recordObj.length) {
439 if (recordObj[0].hasOwnProperty('options')) {
440 var selectedValue = recordObj[0].options[recordObj[0].selectedIndex].value;
441 for (var i = 0; i < values.length; i++) {
442 if (values[i] != selectedValue) {
443 var $recordObject = TYPO3.jQuery(recordObj[0]);
444 this.removeSelectOption($recordObject, values[i]);
445 }
446 }
447 }
448 }
449 },
450 // this function is applied to a newly inserted record by AJAX
451 // it removes the used select items, that should be unique
452 setUnique: function (objectId, recordUid, selectedValue) {
453 if (!this.data.unique || !this.data.unique[objectId]) {
454 return;
455 }
456 var $selector = TYPO3.jQuery('#' + this.escapeObjectId(objectId) + '_selector');
457
458 var unique = this.data.unique[objectId];
459 if (unique.type == 'select') {
460 if (!(unique.selector && unique.max == -1)) {
461 var formName = this.prependFormFieldNames + this.parseObjectId('parts', objectId, 3, 1, true);
462 var formObj = document.getElementsByName(formName);
463 var recordObj = document.getElementsByName(this.prependFormFieldNames + '[' + unique.table + '][' + recordUid + '][' + unique.field + ']');
464 var values = this.getValuesFromHashMap(unique.used);
465 if ($selector.length) {
466 // remove all items from the new select-item which are already used in other children
467 if (recordObj.length) {
468 var $recordObject = TYPO3.jQuery(recordObj[0]);
469 for (var i = 0; i < values.length; i++) {
470 this.removeSelectOption($recordObject, values[i]);
471 }
472 // set the selected item automatically to the first of the remaining items if no selector is used
473 if (!unique.selector) {
474 selectedValue = recordObj[0].options[0].value;
475 recordObj[0].options[0].selected = true;
476 this.updateUnique(recordObj[0], objectId, formName, recordUid);
477 this.handleChangedField(recordObj[0], objectId + '[' + recordUid + ']');
478 }
479 }
480 for (var i = 0; i < values.length; i++) {
481 this.removeSelectOption($selector, values[i]);
482 }
483 if (typeof this.data.unique[objectId].used.length != 'undefined') {
484 this.data.unique[objectId].used = {};
485 }
486 this.data.unique[objectId].used[recordUid] = selectedValue;
487 }
488 // remove the newly used item from each select-field of the child records
489 if (formObj.length && selectedValue) {
490 var records = formObj[0].value.split(',');
491 for (var i = 0; i < records.length; i++) {
492 recordObj = document.getElementsByName(this.prependFormFieldNames + '[' + unique.table + '][' + records[i] + '][' + unique.field + ']');
493 if (recordObj.length && records[i] != recordUid) {
494 var $recordObject = TYPO3.jQuery(recordObj[0]);
495 this.removeSelectOption($recordObject, selectedValue);
496 }
497 }
498 }
499 }
500 } else if (unique.type == 'groupdb') {
501 // add the new record to the used items:
502 this.data.unique[objectId].used[recordUid] = {'table': unique.elTable, 'uid': selectedValue};
503 }
504
505 // remove used items from a selector-box
506 if (unique.selector == 'select' && selectedValue) {
507 this.removeSelectOption($selector, selectedValue);
508 this.data.unique[objectId]['used'][recordUid] = selectedValue;
509 }
510 },
511
512 domAddNewRecord: function (method, insertObjectId, objectPrefix, htmlData) {
513 var $insertObject = TYPO3.jQuery('#' + this.escapeObjectId(insertObjectId));
514 if (this.isBelowMax(objectPrefix)) {
515 if (method == 'bottom') {
516 $insertObject.append(htmlData);
517 } else if (method == 'after') {
518 $insertObject.after(htmlData);
519 }
520 } else {
521 var message = TBE_EDITOR.labels.maxItemsAllowed.replace('{0}', this.data.config[objectPrefix].max);
522 var title = $insertObject.data('title');
523 top.TYPO3.Notification.error(title, message);
524 }
525 },
526
527 domAddRecordDetails: function (objectId, objectPrefix, expandSingle, htmlData) {
528 var hiddenValue, formObj, valueObj;
529 var escapeObjectId = this.escapeObjectId(objectId);
530 var $objectDiv = TYPO3.jQuery('#' + escapeObjectId + '_fields');
531 if ($objectDiv.length == 0 || $objectDiv.html().substr(0, 16) != '<!--notloaded-->') {
532 return;
533 }
534
535 var elName = this.parseObjectId('full', objectId, 2, 0, true);
536
537 var $formObj = TYPO3.jQuery('[name="' + elName + '[hidden]_0"]');
538 var $valueObj = TYPO3.jQuery('[name="' + elName + '[hidden]"]');
539
540 // It might be the case that a child record
541 // cannot be hidden at all (no hidden field)
542 if ($formObj.length && $valueObj.length) {
543 hiddenValue = $formObj[0].checked;
544 $formObj[0].remove();
545 $valueObj[0].remove();
546 }
547
548 // Update DOM
549 $objectDiv.html(htmlData);
550
551 formObj = document.getElementsByName(elName + '[hidden]_0');
552 valueObj = document.getElementsByName(elName + '[hidden]');
553
554 // Set the hidden value again
555 if (formObj.length && valueObj.length) {
556 valueObj[0].value = hiddenValue ? 1 : 0;
557 formObj[0].checked = hiddenValue;
558 }
559
560 // now that the content is loaded, set the expandState
561 this.expandCollapseRecord(objectId, expandSingle);
562 },
563
564 // Get script and link elements from head tag:
565 getDomHeadChildren: function (head) {
566 var headTags = [];
567 TYPO3.jQuery('head script, head link').each(function () {
568 headTags.push(this);
569 });
570 return headTags;
571 },
572
573 getDomHeadTag: function () {
574 if (document && document.head) {
575 return document.head;
576 } else {
577 var $head = TYPO3.jQuery('head');
578 if ($head.length) {
579 return $head.get(0);
580 }
581 }
582 return false;
583 },
584
585 // Search whether elements exist in a given haystack:
586 searchInDomTags: function (haystack, needle) {
587 var result = false;
588 TYPO3.jQuery.each(haystack, function (index, element) {
589 if (element.nodeName.toUpperCase() == needle.name) {
590 var attributesCount = Object.keys(needle.attributes).length;
591 var attributesFound = 0;
592 if (element.getAttribute) {
593 for (var attribute in needle.attributes) {
594 if (needle.attributes.hasOwnProperty(attribute) && element.getAttribute(attribute.key) === attribute.value) {
595 attributesFound++;
596 }
597 }
598 }
599 if (attributesFound === attributesCount) {
600 result = true;
601 return true;
602 }
603 }
604 });
605 return result;
606 },
607
608 // Create a new DOM element:
609 createNewDomElement: function (addTag) {
610 var element = document.createElement(addTag.name);
611 if (addTag.attributes) {
612 TYPO3.jQuery.map(addTag.attributes, function (value, key) {
613 element[key] = value;
614 });
615 }
616 return element;
617 },
618
619 changeSorting: function (objectId, direction) {
620 var objectName = this.prependFormFieldNames + this.parseObjectId('parts', objectId, 3, 2, true);
621 var objectPrefix = this.parseObjectId('full', objectId, 0, 1);
622 var formObj = document.getElementsByName(objectName);
623
624 if (!formObj.length) {
625 return false;
626 }
627
628 // the uid of the calling object (last part in objectId)
629 var callingUid = this.parseObjectId('none', objectId, 1);
630 var records = formObj[0].value.split(',');
631 var current = records.indexOf(callingUid);
632 var changed = false;
633
634 // move up
635 if (direction > 0 && current > 0) {
636 records[current] = records[current - 1];
637 records[current - 1] = callingUid;
638 changed = true;
639
640 // move down
641 } else if (direction < 0 && current < records.length - 1) {
642 records[current] = records[current + 1];
643 records[current + 1] = callingUid;
644 changed = true;
645 }
646
647 if (changed) {
648 formObj[0].value = records.join(',');
649 var cAdj = direction > 0 ? 1 : 0; // adjustment
650 var objectIdPrefix = '#' + this.escapeObjectId(objectPrefix) + this.structureSeparator;
651 TYPO3.jQuery(objectIdPrefix + records[current - cAdj] + '_div').insertBefore(
652 TYPO3.jQuery(objectIdPrefix + records[current + 1 - cAdj] + '_div')
653 );
654 this.redrawSortingButtons(objectPrefix, records);
655 }
656
657 return false;
658 },
659
660 dragAndDropSorting: function (element) {
661 var objectId = element.getAttribute('id').replace(/_records$/, '');
662 var objectName = inline.prependFormFieldNames + inline.parseObjectId('parts', objectId, 3, 0, true);
663 var formObj = document.getElementsByName(objectName);
664 var $element = TYPO3.jQuery(element);
665
666 if (!formObj.length) {
667 return;
668 }
669
670 var checked = [];
671 var order = [];
672 $element.find('.sortableHandle').each(function (i, e) {
673 order.push(TYPO3.jQuery(e).data('id').toString());
674 });
675 var records = formObj[0].value.split(',');
676
677 // check if ordered uid is really part of the records
678 // virtually deleted items might still be there but ordering shouldn't saved at all on them
679 for (var i = 0; i < order.length; i++) {
680 if (records.indexOf(order[i]) != -1) {
681 checked.push(order[i]);
682 }
683 }
684
685 formObj[0].value = checked.join(',');
686
687 if (inline.data.config && inline.data.config[objectId]) {
688 var table = inline.data.config[objectId].table;
689 inline.redrawSortingButtons(objectId + inline.structureSeparator + table, checked);
690 }
691 },
692
693 createDragAndDropSorting: function (objectId) {
694 require(['jquery', 'jquery-ui/sortable'], function ($) {
695 var $sortingContainer = $('#' + inline.escapeObjectId(objectId));
696
697 if ($sortingContainer.hasClass('ui-sortable')) {
698 $sortingContainer.sortable('enable');
699 return;
700 }
701
702 $sortingContainer.sortable(
703 {
704 containment: 'parent',
705 handle: '.sortableHandle',
706 zIndex: '4000',
707 axis: 'y',
708 tolerance: 'pointer',
709 stop: function () {
710 inline.dragAndDropSorting($sortingContainer[0]);
711 }
712 }
713 );
714 });
715 },
716
717 destroyDragAndDropSorting: function (objectId) {
718 require(['jquery', 'jquery-ui/sortable'], function ($) {
719 var $sortingContainer = $('#' + inline.escapeObjectId(objectId));
720 if (!$sortingContainer.hasClass('ui-sortable')) {
721 return;
722 }
723 $sortingContainer.sortable('disable');
724 });
725 },
726
727 redrawSortingButtons: function (objectPrefix, records) {
728 var i, $headerObj, sortUp, sortDown;
729
730 // if no records were passed, fetch them from form field
731 if (typeof records == 'undefined') {
732 records = [];
733 var objectName = this.prependFormFieldNames + this.parseObjectId('parts', objectPrefix, 3, 1, true);
734 var formObj = document.getElementsByName(objectName);
735 if (formObj.length) {
736 records = formObj[0].value.split(',');
737 }
738 }
739
740 for (i = 0; i < records.length; i++) {
741 if (!records[i].length) {
742 continue;
743 }
744
745 $headerObj = TYPO3.jQuery('#' + this.escapeObjectId(objectPrefix) + this.structureSeparator + records[i] + '_header');
746 sortUp = $headerObj.find('.sortingUp');
747 sortDown = $headerObj.find('.sortingDown');
748
749 if (sortUp) {
750 sortUp.css('visibility', (i == 0 ? 'hidden' : 'visible'));
751 }
752 if (sortDown) {
753 sortDown.css('visibility', (i == records.length - 1 ? 'hidden' : 'visible'));
754 }
755 }
756 },
757
758 memorizeAddRecord: function (objectPrefix, newUid, afterUid, selectedValue) {
759 if (this.isBelowMax(objectPrefix)) {
760 var objectName = this.prependFormFieldNames + this.parseObjectId('parts', objectPrefix, 3, 1, true);
761 var formObj = document.getElementsByName(objectName);
762
763 if (formObj.length) {
764 var records = [];
765 if (formObj[0].value.length) {
766 records = formObj[0].value.split(',');
767 }
768
769 if (afterUid) {
770 var newRecords = [];
771 for (var i = 0; i < records.length; i++) {
772 if (records[i].length) {
773 newRecords.push(records[i]);
774 }
775 if (afterUid == records[i]) {
776 newRecords.push(newUid);
777 }
778 }
779 records = newRecords;
780 } else {
781 records.push(newUid);
782 }
783 formObj[0].value = records.join(',');
784 }
785
786 this.redrawSortingButtons(objectPrefix, records);
787
788 if (this.data.unique && this.data.unique[objectPrefix]) {
789 var unique = this.data.unique[objectPrefix];
790 this.setUnique(objectPrefix, newUid, selectedValue);
791 }
792 }
793
794 // if we reached the maximum off possible records after this action, hide the new buttons
795 if (!this.isBelowMax(objectPrefix)) {
796 var objectParent = this.parseObjectId('full', objectPrefix, 0, 1);
797 var md5 = this.getObjectMD5(objectParent);
798 this.hideElementsWithClassName('.inlineNewButton' + (md5 ? '.' + md5 : ''), objectParent);
799 this.hideElementsWithClassName('.inlineForeignSelector' + (md5 ? '.' + md5 : ''), 't3-form-field-item');
800 }
801
802 if (TBE_EDITOR) {
803 TBE_EDITOR.fieldChanged_fName(objectName, formObj);
804 }
805 },
806
807 memorizeRemoveRecord: function (objectName, removeUid) {
808 var formObj = document.getElementsByName(objectName);
809 if (formObj.length) {
810 var parts = [];
811 if (formObj[0].value.length) {
812 parts = formObj[0].value.split(',');
813 parts = parts.without(removeUid);
814 formObj[0].value = parts.join(',');
815 if (TBE_EDITOR) {
816 TBE_EDITOR.fieldChanged_fName(objectName, formObj);
817 }
818 return parts.length;
819 }
820 }
821 return false;
822 },
823
824 updateUnique: function (srcElement, objectPrefix, formName, recordUid) {
825 if (!this.data.unique || !this.data.unique[objectPrefix]) {
826 return;
827 }
828
829 var unique = this.data.unique[objectPrefix];
830 var oldValue = unique.used[recordUid];
831
832 if (unique.selector == 'select') {
833 var selector = $(objectPrefix + '_selector');
834 this.removeSelectOption(selector, srcElement.value);
835 if (typeof oldValue != 'undefined') {
836 this.readdSelectOption(selector, oldValue, unique);
837 }
838 }
839
840 if (unique.selector && unique.max == -1) {
841 return;
842 }
843
844 var formObj = document.getElementsByName(formName);
845 if (!unique || !formObj.length) {
846 return;
847 }
848
849 var records = formObj[0].value.split(',');
850 var recordObj;
851 for (var i = 0; i < records.length; i++) {
852 recordObj = document.getElementsByName(this.prependFormFieldNames + '[' + unique.table + '][' + records[i] + '][' + unique.field + ']');
853 if (recordObj.length && recordObj[0] != srcElement) {
854 var $recordObject = TYPO3.jQuery(recordObj[0]);
855 this.removeSelectOption($recordObject, srcElement.value);
856 if (typeof oldValue != 'undefined') {
857 this.readdSelectOption($recordObject, oldValue, unique);
858 }
859 }
860 }
861 this.data.unique[objectPrefix].used[recordUid] = srcElement.value;
862 },
863
864 revertUnique: function (objectPrefix, elName, recordUid) {
865 if (!this.data.unique || !this.data.unique[objectPrefix]) {
866 return;
867 }
868
869 var unique = this.data.unique[objectPrefix];
870 var fieldObj = elName ? document.getElementsByName(elName + '[' + unique.field + ']') : null;
871
872 if (unique.type == 'select') {
873 if (!fieldObj || !fieldObj.length) {
874 return;
875 }
876
877 delete(this.data.unique[objectPrefix].used[recordUid]);
878
879 if (unique.selector == 'select') {
880 if (!isNaN(fieldObj[0].value)) {
881 var $selector = TYPO3.jQuery('#' + this.escapeObjectId(objectPrefix) + '_selector');
882 this.readdSelectOption($selector, fieldObj[0].value, unique);
883 }
884 }
885
886 if (unique.selector && unique.max == -1) {
887 return;
888 }
889
890 var formName = this.prependFormFieldNames + this.parseObjectId('parts', objectPrefix, 3, 1, true);
891 var formObj = document.getElementsByName(formName);
892 if (!formObj.length) {
893 return;
894 }
895
896 var records = formObj[0].value.split(',');
897 var recordObj;
898 // walk through all inline records on that level and get the select field
899 for (var i = 0; i < records.length; i++) {
900 recordObj = document.getElementsByName(this.prependFormFieldNames + '[' + unique.table + '][' + records[i] + '][' + unique.field + ']');
901 if (recordObj.length) {
902 var $recordObject = TYPO3.jQuery(recordObj[0]);
903 this.readdSelectOption($recordObject, fieldObj[0].value, unique);
904 }
905 }
906 } else if (unique.type == 'groupdb') {
907 // alert(objectPrefix+'/'+recordUid);
908 delete(this.data.unique[objectPrefix].used[recordUid])
909 }
910 },
911
912 enableDisableRecord: function (objectId) {
913 var elName = this.parseObjectId('full', objectId, 2, 0, true) + '[hidden]';
914 var formObj = document.getElementsByName(elName + '_0');
915 var valueObj = document.getElementsByName(elName);
916 var escapedObjectId = this.escapeObjectId(objectId);
917 var $icon = TYPO3.jQuery('#' + escapedObjectId + '_disabled');
918
919 var $container = TYPO3.jQuery('#' + escapedObjectId + '_div');
920
921 // It might be the case that there's no hidden field
922 if (formObj.length && valueObj.length) {
923 formObj[0].click();
924 valueObj[0].value = formObj[0].checked ? 1 : 0;
925 TBE_EDITOR.fieldChanged_fName(elName, elName);
926 }
927
928 if ($icon.length) {
929 if ($icon.hasClass('fa-toggle-on')) {
930 $icon.removeClass('fa-toggle-on');
931 $icon.addClass('fa-toggle-off');
932 $container.addClass('t3-form-field-container-inline-hidden');
933 } else {
934 $icon.removeClass('fa-toggle-off');
935 $icon.addClass('fa-toggle-on');
936 $container.removeClass('t3-form-field-container-inline-hidden');
937 }
938 }
939
940 return false;
941 },
942
943 deleteRecord: function (objectId, options) {
944 var i, j, inlineRecords, records, childObjectId, childTable;
945 var objectPrefix = this.parseObjectId('full', objectId, 0, 1);
946 var elName = this.parseObjectId('full', objectId, 2, 0, true);
947 var shortName = this.parseObjectId('parts', objectId, 2, 0, true);
948 var recordUid = this.parseObjectId('none', objectId, 1);
949 var beforeDeleteIsBelowMax = this.isBelowMax(objectPrefix);
950
951 // revert the unique settings if available
952 this.revertUnique(objectPrefix, elName, recordUid);
953
954 // Remove from TBE_EDITOR (required fields, required range, etc.):
955 if (TBE_EDITOR && TBE_EDITOR.removeElement) {
956 var removeStack = [];
957 // Iterate over all child records:
958 inlineRecords = TYPO3.jQuery('.inlineRecord', '#' + objectId + '_div');
959 // Remove nested child records from TBE_EDITOR required/range checks:
960 for (i = inlineRecords.length - 1; i >= 0; i--) {
961 if (inlineRecords.get(i).value.length) {
962 records = inlineRecords.get(i).value.split(',');
963 childObjectId = this.data.map[inlineRecords.get(i).name];
964 childTable = this.data.config[childObjectId].table;
965 for (j = records.length - 1; j >= 0; j--) {
966 removeStack.push(this.prependFormFieldNames + '[' + childTable + '][' + records[j] + ']');
967 }
968 }
969 }
970 removeStack.push(this.prependFormFieldNames + shortName);
971 TBE_EDITOR.removeElementArray(removeStack);
972 }
973
974 // Mark this container as deleted
975 TYPO3.jQuery('#' + this.escapeObjectId(objectId) + '_div').addClass('inlineIsDeletedRecord');
976
977 // If the record is new and was never saved before, just remove it from DOM:
978 if (this.isNewRecord(objectId) || options && options.forceDirectRemoval) {
979 this.fadeAndRemove(objectId + '_div');
980 // If the record already exists in storage, mark it to be deleted on clicking the save button:
981 } else {
982 document.getElementsByName('cmd' + shortName + '[delete]')[0].disabled = false;
983 TYPO3.jQuery('#' + objectId + '_div').fadeOut();
984 }
985
986 var recordCount = this.memorizeRemoveRecord(
987 this.prependFormFieldNames + this.parseObjectId('parts', objectId, 3, 2, true),
988 recordUid
989 );
990
991 if (recordCount <= 1) {
992 this.destroyDragAndDropSorting(this.parseObjectId('full', objectId, 0, 2) + '_records');
993 }
994 this.redrawSortingButtons(objectPrefix);
995
996 // if the NEW-button was hidden and now we can add again new children, show the button
997 if (!beforeDeleteIsBelowMax && this.isBelowMax(objectPrefix)) {
998 var objectParent = this.parseObjectId('full', objectPrefix, 0, 1);
999 var md5 = this.getObjectMD5(objectParent);
1000 this.showElementsWithClassName('.inlineNewButton' + (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 = this.prependFormFieldNames + '[' + 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 = this.prependFormFieldNames + 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 = this.prependFormFieldNames + 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);