62940ed6a8c3e533b6c5c39cfe305415a3c44b34
4 * This file is part of the TYPO3 CMS project.
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.
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
13 * The TYPO3 project - inspiring people to share!
17 * Inline-Relational-Record Editing
21 classVisible
: 't3-form-field-container-inline-visible',
22 classCollapsed
: 't3-form-field-container-inline-collapsed',
23 structureSeparator
: '-',
24 flexFormSeparator
: '---',
25 flexFormSubstitute
: ':',
26 prependFormFieldNames
: 'data',
27 noTitleString
: '[No title]',
33 addToDataArray: function (object
) {
34 TYPO3
.jQuery
.each(object
, function (key
, value
) {
35 if (!inline
.data
[key
]) {
36 inline
.data
[key
] = {};
38 TYPO3
.jQuery
.extend(inline
.data
[key
], value
);
41 setPrependFormFieldNames: function (value
) {
42 this.prependFormFieldNames
= value
;
44 setNoTitleString: function (value
) {
45 this.noTitleString
= value
;
47 toggleEvent: function (event
) {
48 var $triggerElement
= TYPO3
.jQuery(event
.target
);
49 if ($triggerElement
.parents('.t3-form-field-header-inline-ctrl').length
== 1) {
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')
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
);
65 var $currentObject
= TYPO3
.jQuery('#' + escapedObjectId
+ '_div');
66 // if content is not loaded yet, get it now from server
67 if (inline
.isLoading
|| (TYPO3
.jQuery('#' + escapedObjectId
+ '_fields').length
> 0 && TYPO3
.jQuery("#irre-loading-indicator" + escapedObjectId
).length
> 0)) {
69 } else if (TYPO3
.jQuery('#' + escapedObjectId
+ '_fields').length
> 0 && TYPO3
.jQuery('#' + escapedObjectId
+ '_fields').html().substr(0, 16) == '<!--notloaded-->') {
70 inline
.isLoading
= true;
71 // add loading-indicator
72 if (TYPO3
.jQuery('#' + escapedObjectId
+ '_loadingbar').length
=== 0) {
73 var loadingBar
= '<div id="' + objectId
+ '_loadingbar" class="t3-form-header-inline-loadingbar loadingbar"><div class="expand"></div></div>';
74 TYPO3
.jQuery('#' + escapedObjectId
+ '_header').after(loadingBar
);
76 return this.getRecordDetails(objectId
, returnURL
);
79 var isCollapsed
= $currentObject
.hasClass(this.classCollapsed
);
83 // if only a single record should be visibly for that set of records
84 // and the record clicked itself is no visible, collapse all others
85 if (expandSingle
&& $currentObject
.hasClass(this.classCollapsed
)) {
86 collapse
= this.collapseAllRecords(objectId
, objectPrefix
, currentUid
);
89 inline
.toggleElement(objectId
);
91 if (this.isNewRecord(objectId
)) {
92 this.updateExpandedCollapsedStateLocally(objectId
, isCollapsed
);
93 } else if (isCollapsed
) {
94 expand
.push(currentUid
);
95 } else if (!isCollapsed
) {
96 collapse
.push(currentUid
);
99 this.setExpandedCollapsedState(objectId
, expand
.join(','), collapse
.join(','));
104 toggleElement: function (objectId
) {
105 var escapedObjectId
= this.escapeObjectId(objectId
);
106 var $jQueryObject
= TYPO3
.jQuery('#' + escapedObjectId
+ '_div');
108 if ($jQueryObject
.hasClass(this.classCollapsed
)) {
109 $jQueryObject
.removeClass(this.classCollapsed
).addClass(this.classVisible
);
110 $jQueryObject
.find('#' + escapedObjectId
+ '_header .t3-icon-irre-collapsed').removeClass('t3-icon-irre-collapsed').addClass('t3-icon-irre-expanded');
112 $jQueryObject
.removeClass(this.classVisible
).addClass(this.classCollapsed
);
113 $jQueryObject
.find('#' + escapedObjectId
+ '_header .t3-icon-irre-expanded').addClass('t3-icon-irre-collapsed').removeClass('t3-icon-irre-expanded');
116 collapseAllRecords: function (objectId
, objectPrefix
, callingUid
) {
117 // get the form field, where all records are stored
118 var objectName
= this.prependFormFieldNames
+ this.parseObjectId('parts', objectId
, 3, 2, true);
119 var formObj
= document
.getElementsByName(objectName
);
122 if (formObj
.length
) {
123 // the uid of the calling object (last part in objectId)
124 var recObjectId
= '', escapedRecordObjectId
;
126 var records
= formObj
[0].value
.split(',');
127 for (var i
= 0; i
< records
.length
; i
++) {
128 recObjectId
= objectPrefix
+ this.structureSeparator
+ records
[i
];
129 escapedRecordObjectId
= this.escapeObjectId(recObjectId
);
131 var $recordEntry
= TYPO3
.jQuery('#' + escapedRecordObjectId
);
132 if (records
[i
] != callingUid
&& $recordEntry
.hasClass(this.classVisible
)) {
133 TYPO3
.jQuery('#' + escapedRecordObjectId
+ '_div').removeClass(this.classVisible
).addClass(this.classCollapsed
);
134 if (this.isNewRecord(recObjectId
)) {
135 this.updateExpandedCollapsedStateLocally(recObjectId
, 0);
137 collapse
.push(records
[i
]);
146 updateExpandedCollapsedStateLocally: function (objectId
, value
) {
147 var ucName
= 'uc[inlineView]' + this.parseObjectId('parts', objectId
, 3, 2, true);
148 var ucFormObj
= document
.getElementsByName(ucName
);
149 if (ucFormObj
.length
) {
150 ucFormObj
[0].value
= value
;
154 getRecordDetails: function (objectId
, returnURL
) {
155 var context
= this.getContext(this.parseObjectId('full', objectId
, 0, 1));
156 inline
.makeAjaxCall('getRecordDetails', [inline
.getNumberOfRTE(), objectId
, returnURL
], true, context
);
160 createNewRecord: function (objectId
, recordUid
) {
161 if (this.isBelowMax(objectId
)) {
162 var context
= this.getContext(objectId
);
164 objectId
+= this.structureSeparator
+ recordUid
;
166 this.makeAjaxCall('createNewRecord', [this.getNumberOfRTE(), objectId
], true, context
);
168 alert('There are no more relations possible at this moment!');
173 synchronizeLocalizeRecords: function (objectId
, type
) {
174 var context
= this.getContext(objectId
);
175 var parameters
= [this.getNumberOfRTE(), objectId
, type
];
176 this.makeAjaxCall('synchronizeLocalizeRecords', parameters
, true, context
);
179 setExpandedCollapsedState: function (objectId
, expand
, collapse
) {
180 var context
= this.getContext(objectId
);
181 this.makeAjaxCall('setExpandedCollapsedState', [objectId
, expand
, collapse
], false, context
);
184 makeAjaxCall: function (method
, params
, lock
, context
) {
185 var url
= '', urlParams
= '', options
= {};
186 if (method
&& params
&& params
.length
&& this.lockAjaxMethod(method
, lock
)) {
187 url
= TBE_EDITOR
.getBackendPath() + TYPO3
.settings
.ajaxUrls
['t3lib_TCEforms_inline::' + method
];
189 for (var i
= 0, max
= params
.length
; i
< max
; i
++) {
190 urlParams
+= '&ajax[' + i
+ ']=' + encodeURIComponent(params
[i
]);
193 urlParams
+= '&ajax[context]=' + encodeURIComponent(Object
.toJSON(context
));
198 success: function (data
, message
, jqXHR
) {
199 inline
.isLoading
= false;
200 inline
.processAjaxResponse(method
, jqXHR
);
202 error: function (jqXHR
, statusText
, errorThrown
) {
203 inline
.isLoading
= false;
204 inline
.showAjaxFailure(method
, jqXHR
);
208 TYPO3
.jQuery
.ajax(url
, options
);
212 lockAjaxMethod: function (method
, lock
) {
213 if (!lock
|| !inline
.lockedAjaxMethod
[method
]) {
214 inline
.lockedAjaxMethod
[method
] = true;
221 unlockAjaxMethod: function (method
) {
222 inline
.lockedAjaxMethod
[method
] = false;
225 processAjaxResponse: function (method
, xhr
, json
) {
226 var addTag
= null, restart
= false, processedCount
= 0, element
= null, errorCatch
= [], sourcesWaiting
= [];
228 json
= xhr
.responseJSON
;
230 // If there are elements the should be added to the <HEAD> tag (e.g. for RTEhtmlarea):
232 var head
= inline
.getDomHeadTag();
233 var headTags
= inline
.getDomHeadChildren(head
);
234 TYPO3
.jQuery
.each(json
.headData
, function (index
, addTag
) {
236 if (addTag
&& (addTag
.innerHTML
|| !inline
.searchInDomTags(headTags
, addTag
))) {
237 if (addTag
.name
== 'SCRIPT' && addTag
.innerHTML
&& processedCount
) {
241 if (addTag
.name
== 'SCRIPT' && addTag
.innerHTML
) {
243 eval(addTag
.innerHTML
);
248 element
= inline
.createNewDomElement(addTag
);
249 // Set onload handler for external JS scripts:
250 if (addTag
.name
== 'SCRIPT' && element
.src
) {
251 element
.onload
= inline
.sourceLoadedHandler(element
);
252 sourcesWaiting
.push(element
.src
);
254 head
.appendChild(element
);
257 json
.headData
.shift();
263 if (restart
|| processedCount
) {
264 window
.setTimeout(function () {
265 inline
.reprocessAjaxResponse(method
, json
, sourcesWaiting
);
269 inline
.unlockAjaxMethod(method
);
271 if (json
.scriptCall
&& json
.scriptCall
.length
> 0) {
272 TYPO3
.jQuery
.each(json
.scriptCall
, function (index
, value
) {
276 TYPO3
.TCEFORMS
.convertDateFieldsToDatePicker();
280 // Check if dynamically added scripts are loaded and restart inline.processAjaxResponse():
281 reprocessAjaxResponse: function (method
, json
, sourcesWaiting
) {
282 var sourcesLoaded
= true;
283 if (sourcesWaiting
&& sourcesWaiting
.length
) {
284 TYPO3
.jQuery
.each(sourcesWaiting
, function (index
, source
) {
285 if (!inline
.sourcesLoaded
[source
]) {
286 sourcesLoaded
= false;
292 TYPO3
.jQuery
.each(sourcesWaiting
, function (index
, source
) {
293 delete(inline
.sourcesLoaded
[source
]);
295 window
.setTimeout(function () {
296 inline
.processAjaxResponse(method
, null, json
);
299 window
.setTimeout(function () {
300 inline
.reprocessAjaxResponse(method
, json
, sourcesWaiting
);
305 sourceLoadedHandler: function (element
) {
306 if (element
&& element
.src
) {
307 inline
.sourcesLoaded
[element
.src
] = true;
311 showAjaxFailure: function (method
, xhr
) {
312 inline
.unlockAjaxMethod(method
);
313 alert('Error: ' + xhr
.status
+ "\n" + xhr
.statusText
);
316 // foreign_selector: used by selector box (type='select')
317 importNewRecord: function (objectId
) {
318 var $selector
= TYPO3
.jQuery('#' + this.escapeObjectId(objectId
) + '_selector');
319 var selectedIndex
= $selector
.prop('selectedIndex');
320 if (selectedIndex
!= -1) {
321 var context
= this.getContext(objectId
);
322 var selectedValue
= $selector
.val();
323 if (!this.data
.unique
|| !this.data
.unique
[objectId
]) {
324 $selector
.find('option').eq(selectedIndex
).prop('selected', false);
326 this.makeAjaxCall('createNewRecord', [this.getNumberOfRTE(), objectId
, selectedValue
], true, context
);
331 // foreign_selector: used by element browser (type='group/db')
332 importElement: function (objectId
, table
, uid
, type
) {
333 var context
= this.getContext(objectId
);
334 inline
.makeAjaxCall('createNewRecord', [inline
.getNumberOfRTE(), objectId
, uid
], true, context
);
337 importElementMultiple: function (objectId
, table
, uidArray
, type
) {
338 uidArray
.each(function (uid
) {
339 inline
.delayedImportElement(objectId
, table
, uid
, type
);
342 delayedImportElement: function (objectId
, table
, uid
, type
) {
343 if (inline
.lockedAjaxMethod
['createNewRecord'] == true) {
344 window
.setTimeout("inline.delayedImportElement('" + objectId
+ "','" + table
+ "'," + uid
+ ", null );",
347 inline
.importElement(objectId
, table
, uid
, type
);
350 // Check uniqueness for element browser:
351 checkUniqueElement: function (objectId
, table
, uid
, type
) {
352 if (this.checkUniqueUsed(objectId
, uid
, table
)) {
353 return {passed
: false, message
: 'There is already a relation to the selected element!'};
355 return {passed
: true};
359 // Checks if a record was used and should be unique:
360 checkUniqueUsed: function (objectId
, uid
, table
) {
361 if (this.data
.unique
&& this.data
.unique
[objectId
]) {
362 var unique
= this.data
.unique
[objectId
];
363 var values
= this.getValuesFromHashMap(unique
.used
);
365 // for select: only the uid is stored
366 if (unique
['type'] == 'select') {
367 if (values
.indexOf(uid
) != -1) {
371 // for group/db: table and uid is stored in a assoc array
372 } else if (unique
.type
== 'groupdb') {
373 for (var i
= values
.length
- 1; i
>= 0; i
--) {
374 // if the pair table:uid is already used:
375 if (values
[i
].table
== table
&& values
[i
].uid
== uid
) {
384 setUniqueElement: function (objectId
, table
, uid
, type
, elName
) {
385 var recordUid
= this.parseFormElementName('none', elName
, 1, 1);
386 // alert(objectId+'/'+table+'/'+uid+'/'+recordUid);
387 this.setUnique(objectId
, recordUid
, uid
);
390 getKeysFromHashMap: function (unique
) {
391 return TYPO3
.jQuery
.map(unique
, function(value
, key
) {
396 getValuesFromHashMap: function (hashMap
) {
397 return TYPO3
.jQuery
.map(hashMap
, function(value
, key
) {
402 // Remove all select items already used
403 // from a newly retrieved/expanded record
404 removeUsed: function (objectId
, recordUid
) {
405 if (this.data
.unique
&& this.data
.unique
[objectId
]) {
406 var unique
= this.data
.unique
[objectId
];
407 if (unique
.type
== 'select') {
408 var formName
= this.prependFormFieldNames
+ this.parseObjectId('parts', objectId
, 3, 1, true);
409 var formObj
= document
.getElementsByName(formName
);
410 var recordObj
= document
.getElementsByName(this.prependFormFieldNames
+ '[' + unique
.table
+ '][' + recordUid
+ '][' + unique
.field
+ ']');
411 var values
= this.getValuesFromHashMap(unique
.used
);
412 if (recordObj
.length
) {
413 if (recordObj
[0].hasOwnProperty('options')) {
414 var selectedValue
= recordObj
[0].options
[recordObj
[0].selectedIndex
].value
;
415 for (var i
=0; i
<values
.length
; i
++) {
416 if (values
[i
] != selectedValue
) {
417 this.removeSelectOption(recordObj
[0], values
[i
]);
425 // this function is applied to a newly inserted record by AJAX
426 // it removes the used select items, that should be unique
427 setUnique: function (objectId
, recordUid
, selectedValue
) {
428 if (this.data
.unique
&& this.data
.unique
[objectId
]) {
429 var $selector
= TYPO3
.jQuery('#' + this.escapeObjectId(objectId
) + '_selector');
430 var unique
= this.data
.unique
[objectId
];
431 if (unique
.type
== 'select') {
432 if (!(unique
.selector
&& unique
.max
== -1)) {
433 var formName
= this.prependFormFieldNames
+ this.parseObjectId('parts', objectId
, 3, 1, true);
434 var formObj
= document
.getElementsByName(formName
);
435 var recordObj
= document
.getElementsByName(this.prependFormFieldNames
+ '[' + unique
.table
+ '][' + recordUid
+ '][' + unique
.field
+ ']');
436 var values
= this.getValuesFromHashMap(unique
.used
);
437 if ($selector
.length
) {
438 // remove all items from the new select-item which are already used in other children
439 if (recordObj
.length
) {
440 for (var i
= 0; i
< values
.length
; i
++) {
441 this.removeSelectOption(recordObj
[0], values
[i
]);
443 // set the selected item automatically to the first of the remaining items if no selector is used
444 if (!unique
.selector
) {
445 selectedValue
= recordObj
[0].options
[0].value
;
446 recordObj
[0].options
[0].selected
= true;
447 this.updateUnique(recordObj
[0], objectId
, formName
, recordUid
);
448 this.handleChangedField(recordObj
[0], objectId
+ '[' + recordUid
+ ']');
451 for (var i
= 0; i
< values
.length
; i
++) {
452 this.removeSelectOption($selector
, values
[i
]);
454 if (typeof this.data
.unique
[objectId
].used
.length
!= 'undefined') {
455 this.data
.unique
[objectId
].used
= {};
457 this.data
.unique
[objectId
].used
[recordUid
] = selectedValue
;
459 // remove the newly used item from each select-field of the child records
460 if (formObj
.length
&& selectedValue
) {
461 var records
= formObj
[0].value
.split(',');
462 for (var i
= 0; i
< records
.length
; i
++) {
463 recordObj
= document
.getElementsByName(this.prependFormFieldNames
+ '[' + unique
.table
+ '][' + records
[i
] + '][' + unique
.field
+ ']');
464 if (recordObj
.length
&& records
[i
] != recordUid
) {
465 this.removeSelectOption(TYPO3
.jQuery(recordObj
[0]), selectedValue
);
470 } else if (unique
.type
== 'groupdb') {
471 // add the new record to the used items:
472 this.data
.unique
[objectId
].used
[recordUid
] = {'table': unique
.elTable
, 'uid': selectedValue
};
475 // remove used items from a selector-box
476 if (unique
.selector
== 'select' && selectedValue
) {
477 this.removeSelectOption($selector
, selectedValue
);
478 this.data
.unique
[objectId
]['used'][recordUid
] = selectedValue
;
483 domAddNewRecord: function (method
, insertObjectId
, objectPrefix
, htmlData
) {
484 if (this.isBelowMax(objectPrefix
)) {
485 var $insertObject
= TYPO3
.jQuery('#' + this.escapeObjectId(insertObjectId
));
486 if (method
== 'bottom') {
487 $insertObject
.append(htmlData
);
488 } else if (method
== 'after') {
489 $insertObject
.after(htmlData
);
493 domAddRecordDetails: function (objectId
, objectPrefix
, expandSingle
, htmlData
) {
494 var hiddenValue
, formObj
, valueObj
;
495 var escapeObjectId
= this.escapeObjectId(objectId
);
496 var $objectDiv
= TYPO3
.jQuery('#' + escapeObjectId
+ '_fields');
497 if ($objectDiv
.length
== 0 || $objectDiv
.html().substr(0, 16) != '<!--notloaded-->') {
501 var elName
= this.parseObjectId('full', objectId
, 2, 0, true);
503 var $formObj
= TYPO3
.jQuery('[name="' + elName
+ '[hidden]_0"]');
504 var $valueObj
= TYPO3
.jQuery('[name="' + elName
+ '[hidden]"]');
506 // It might be the case that a child record
507 // cannot be hidden at all (no hidden field)
508 if ($formObj
.length
&& $valueObj
.length
) {
509 hiddenValue
= $formObj
[0].checked
;
510 $formObj
[0].remove();
511 $valueObj
[0].remove();
515 $objectDiv
.html(htmlData
);
517 formObj
= document
.getElementsByName(elName
+ '[hidden]_0');
518 valueObj
= document
.getElementsByName(elName
+ '[hidden]');
520 // Set the hidden value again
521 if (formObj
.length
&& valueObj
.length
) {
522 valueObj
[0].value
= hiddenValue
? 1 : 0;
523 formObj
[0].checked
= hiddenValue
;
526 // remove loading-indicator
527 TYPO3
.jQuery('#' + escapeObjectId
+ '_loadingbar').remove();
529 // now that the content is loaded, set the expandState
530 this.expandCollapseRecord(objectId
, expandSingle
);
533 // Get script and link elements from head tag:
534 getDomHeadChildren: function (head
) {
536 TYPO3
.jQuery('head script, head link').each(function () {
542 getDomHeadTag: function () {
543 if (document
&& document
.head
) {
544 return document
.head
;
546 var $head
= TYPO3
.jQuery('head');
554 // Search whether elements exist in a given haystack:
555 searchInDomTags: function (haystack
, needle
) {
557 TYPO3
.jQuery
.each(haystack
, function (index
, element
) {
558 if (element
.nodeName
.toUpperCase() == needle
.name
) {
559 var attributesCount
= $H(needle
.attributes
).keys().length
;
560 var attributesFound
= 0;
561 $H(needle
.attributes
).each(function (attribute
) {
562 if (element
.getAttribute
&& element
.getAttribute(attribute
.key
) == attribute
.value
) {
566 if (attributesFound
== attributesCount
) {
575 // Create a new DOM element:
576 createNewDomElement: function (addTag
) {
577 var element
= document
.createElement(addTag
.name
);
578 if (addTag
.attributes
) {
579 TYPO3
.jQuery
.map(addTag
.attributes
, function (value
, key
) {
580 element
[key
] = value
;
586 changeSorting: function (objectId
, direction
) {
587 var objectName
= this.prependFormFieldNames
+ this.parseObjectId('parts', objectId
, 3, 2, true);
588 var objectPrefix
= this.parseObjectId('full', objectId
, 0, 1);
589 var formObj
= document
.getElementsByName(objectName
);
591 if (formObj
.length
) {
592 // the uid of the calling object (last part in objectId)
593 var callingUid
= this.parseObjectId('none', objectId
, 1);
594 var records
= formObj
[0].value
.split(',');
595 var current
= records
.indexOf(callingUid
);
599 if (direction
> 0 && current
> 0) {
600 records
[current
] = records
[current
- 1];
601 records
[current
- 1] = callingUid
;
605 } else if (direction
< 0 && current
< records
.length
- 1) {
606 records
[current
] = records
[current
+ 1];
607 records
[current
+ 1] = callingUid
;
612 formObj
[0].value
= records
.join(',');
613 var cAdj
= direction
> 0 ? 1 : 0; // adjustment
614 var objectIdPrefix
= '#' + this.escapeObjectId(objectPrefix
) + this.structureSeparator
;
615 TYPO3
.jQuery(objectIdPrefix
+ records
[current
- cAdj
] + '_div').insertBefore(
616 TYPO3
.jQuery(objectIdPrefix
+ records
[current
+ 1 - cAdj
] + '_div')
618 this.redrawSortingButtons(objectPrefix
, records
);
625 dragAndDropSorting: function (element
) {
626 require(['jquery'], function ($) {
627 var objectId
= element
.getAttribute('id').replace(/_records$/, '');
628 var objectName
= inline
.prependFormFieldNames
+ inline
.parseObjectId('parts', objectId
, 3, 0, true);
629 var formObj
= document
.getElementsByName(objectName
);
630 var $element
= TYPO3
.jQuery(element
);
632 if (formObj
.length
) {
635 $element
.find('.sortableHandle').each(function (i
, e
) {
636 order
.push($(e
).data('id').toString());
638 var records
= formObj
[0].value
.split(',');
640 // check if ordered uid is really part of the records
641 // virtually deleted items might still be there but ordering shouldn't saved at all on them
642 for (var i
= 0; i
< order
.length
; i
++) {
643 if (records
.indexOf(order
[i
]) != -1) {
644 checked
.push(order
[i
]);
648 formObj
[0].value
= checked
.join(',');
650 if (inline
.data
.config
&& inline
.data
.config
[objectId
]) {
651 var table
= inline
.data
.config
[objectId
].table
;
652 inline
.redrawSortingButtons(objectId
+ inline
.structureSeparator
+ table
, checked
);
658 createDragAndDropSorting: function (objectId
) {
659 require(['jquery', 'jquery-ui/jquery-ui-1.10.4.custom.min'], function ($) {
660 var $sortingContainer
= $('#' + inline
.escapeObjectId(objectId
));
662 if ($sortingContainer
.hasClass('ui-sortable')) {
663 $sortingContainer
.sortable('enable');
667 $sortingContainer
.addClass('t3-form-field-container-wrap');
668 $sortingContainer
.sortable(
671 handle
: '.sortableHandle',
674 tolerance
: 'pointer',
676 inline
.dragAndDropSorting($sortingContainer
[0]);
683 destroyDragAndDropSorting: function (objectId
) {
684 require(['jquery', 'jquery-ui/jquery-ui-1.10.4.custom.min'], function ($) {
685 var $sortingContainer
= $('#' + inline
.escapeObjectId(objectId
));
686 if (!$sortingContainer
.hasClass('ui-sortable')) {
689 $sortingContainer
.sortable('disable');
693 redrawSortingButtons: function (objectPrefix
, records
) {
694 var i
, $headerObj
, sortUp
, sortDown
;
696 // if no records were passed, fetch them from form field
697 if (typeof records
== 'undefined') {
699 var objectName
= this.prependFormFieldNames
+ this.parseObjectId('parts', objectPrefix
, 3, 1, true);
700 var formObj
= document
.getElementsByName(objectName
);
701 if (formObj
.length
) {
702 records
= formObj
[0].value
.split(',');
706 for (i
= 0; i
< records
.length
; i
++) {
707 if (!records
[i
].length
) {
711 $headerObj
= TYPO3
.jQuery('#' + this.escapeObjectId(objectPrefix
) + this.structureSeparator
+ records
[i
] + '_header');
712 sortUp
= $headerObj
.find('.sortingUp');
713 sortDown
= $headerObj
.find('.sortingDown');
716 sortUp
.css('visibility', (i
== 0 ? 'hidden' : 'visible'));
719 sortDown
.css('visibility', (i
== records
.length
- 1 ? 'hidden' : 'visible'));
724 memorizeAddRecord: function (objectPrefix
, newUid
, afterUid
, selectedValue
) {
725 if (this.isBelowMax(objectPrefix
)) {
726 var objectName
= this.prependFormFieldNames
+ this.parseObjectId('parts', objectPrefix
, 3, 1, true);
727 var formObj
= document
.getElementsByName(objectName
);
729 if (formObj
.length
) {
731 if (formObj
[0].value
.length
) {
732 records
= formObj
[0].value
.split(',');
737 for (var i
= 0; i
< records
.length
; i
++) {
738 if (records
[i
].length
) {
739 newRecords
.push(records
[i
]);
741 if (afterUid
== records
[i
]) {
742 newRecords
.push(newUid
);
745 records
= newRecords
;
747 records
.push(newUid
);
749 formObj
[0].value
= records
.join(',');
752 this.redrawSortingButtons(objectPrefix
, records
);
754 if (this.data
.unique
&& this.data
.unique
[objectPrefix
]) {
755 var unique
= this.data
.unique
[objectPrefix
];
756 this.setUnique(objectPrefix
, newUid
, selectedValue
);
760 // if we reached the maximum off possible records after this action, hide the new buttons
761 if (!this.isBelowMax(objectPrefix
)) {
762 var objectParent
= this.parseObjectId('full', objectPrefix
, 0, 1);
763 var md5
= this.getObjectMD5(objectParent
);
764 this.hideElementsWithClassName('.inlineNewButton' + (md5
? '.' + md5
: ''), objectParent
);
768 TBE_EDITOR
.fieldChanged_fName(objectName
, formObj
);
772 memorizeRemoveRecord: function (objectName
, removeUid
) {
773 var formObj
= document
.getElementsByName(objectName
);
774 if (formObj
.length
) {
776 if (formObj
[0].value
.length
) {
777 parts
= formObj
[0].value
.split(',');
778 parts
= parts
.without(removeUid
);
779 formObj
[0].value
= parts
.join(',');
781 TBE_EDITOR
.fieldChanged_fName(objectName
, formObj
);
789 updateUnique: function (srcElement
, objectPrefix
, formName
, recordUid
) {
790 if (this.data
.unique
&& this.data
.unique
[objectPrefix
]) {
791 var unique
= this.data
.unique
[objectPrefix
];
792 var oldValue
= unique
.used
[recordUid
];
794 if (unique
.selector
== 'select') {
795 var selector
= $(objectPrefix
+ '_selector');
796 this.removeSelectOption(selector
, srcElement
.value
);
797 if (typeof oldValue
!= 'undefined') {
798 this.readdSelectOption(selector
, oldValue
, unique
);
802 if (!(unique
.selector
&& unique
.max
== -1)) {
803 var formObj
= document
.getElementsByName(formName
);
804 if (unique
&& formObj
.length
) {
805 var records
= formObj
[0].value
.split(',');
807 for (var i
= 0; i
< records
.length
; i
++) {
808 recordObj
= document
.getElementsByName(this.prependFormFieldNames
+ '[' + unique
.table
+ '][' + records
[i
] + '][' + unique
.field
+ ']');
809 if (recordObj
.length
&& recordObj
[0] != srcElement
) {
810 this.removeSelectOption(recordObj
[0], srcElement
.value
);
811 if (typeof oldValue
!= 'undefined') {
812 this.readdSelectOption(recordObj
[0], oldValue
, unique
);
816 this.data
.unique
[objectPrefix
].used
[recordUid
] = srcElement
.value
;
822 revertUnique: function (objectPrefix
, elName
, recordUid
) {
823 if (this.data
.unique
&& this.data
.unique
[objectPrefix
]) {
824 var unique
= this.data
.unique
[objectPrefix
];
825 var fieldObj
= elName
? document
.getElementsByName(elName
+ '[' + unique
.field
+ ']') : null;
827 if (unique
.type
== 'select') {
828 if (fieldObj
&& fieldObj
.length
) {
829 delete(this.data
.unique
[objectPrefix
].used
[recordUid
]);
831 if (unique
.selector
== 'select') {
832 if (!isNaN(fieldObj
[0].value
)) {
833 var $selector
= TYPO3
.jQuery('#' + objectPrefix
+ '_selector');
834 this.readdSelectOption($selector
, fieldObj
[0].value
, unique
);
838 if (!(unique
.selector
&& unique
.max
== -1)) {
839 var formName
= this.prependFormFieldNames
+ this.parseObjectId('parts', objectPrefix
, 3, 1, true);
840 var formObj
= document
.getElementsByName(formName
);
841 if (formObj
.length
) {
842 var records
= formObj
[0].value
.split(',');
844 // walk through all inline records on that level and get the select field
845 for (var i
= 0; i
< records
.length
; i
++) {
846 recordObj
= document
.getElementsByName(this.prependFormFieldNames
+ '[' + unique
.table
+ '][' + records
[i
] + '][' + unique
.field
+ ']');
847 if (recordObj
.length
) {
848 this.readdSelectOption(recordObj
[0], fieldObj
[0].value
, unique
);
854 } else if (unique
.type
== 'groupdb') {
855 // alert(objectPrefix+'/'+recordUid);
856 delete(this.data
.unique
[objectPrefix
].used
[recordUid
])
861 enableDisableRecord: function (objectId
) {
862 var elName
= this.parseObjectId('full', objectId
, 2, 0, true) + '[hidden]';
863 var formObj
= document
.getElementsByName(elName
+ '_0');
864 var valueObj
= document
.getElementsByName(elName
);
865 var escapedObjectId
= this.escapeObjectId(objectId
);
866 var $icon
= TYPO3
.jQuery('#' + escapedObjectId
+ '_disabled');
868 var $container
= TYPO3
.jQuery('#' + escapedObjectId
+ '_div');
870 // It might be the case that there's no hidden field
871 if (formObj
.length
&& valueObj
.length
) {
873 valueObj
[0].value
= formObj
[0].checked
? 1 : 0;
874 TBE_EDITOR
.fieldChanged_fName(elName
, elName
);
878 if ($icon
.hasClass('t3-icon-edit-hide')) {
879 $icon
.removeClass('t3-icon-edit-hide');
880 $icon
.addClass('t3-icon-edit-unhide');
881 $container
.addClass('t3-form-field-container-inline-hidden');
883 $icon
.removeClass('t3-icon-edit-unhide');
884 $icon
.addClass('t3-icon-edit-hide');
885 $container
.removeClass('t3-form-field-container-inline-hidden');
892 deleteRecord: function (objectId
, options
) {
893 var i
, j
, inlineRecords
, records
, childObjectId
, childTable
;
894 var objectPrefix
= this.parseObjectId('full', objectId
, 0, 1);
895 var elName
= this.parseObjectId('full', objectId
, 2, 0, true);
896 var shortName
= this.parseObjectId('parts', objectId
, 2, 0, true);
897 var recordUid
= this.parseObjectId('none', objectId
, 1);
898 var beforeDeleteIsBelowMax
= this.isBelowMax(objectPrefix
);
900 // revert the unique settings if available
901 this.revertUnique(objectPrefix
, elName
, recordUid
);
903 // Remove from TBE_EDITOR (required fields, required range, etc.):
904 if (TBE_EDITOR
&& TBE_EDITOR
.removeElement
) {
905 var removeStack
= [];
906 // Iterate over all child records:
907 inlineRecords
= Element
.select(objectId
+ '_div', '.inlineRecord');
908 // Remove nested child records from TBE_EDITOR required/range checks:
909 for (i
= inlineRecords
.length
- 1; i
>= 0; i
--) {
910 if (inlineRecords
[i
].value
.length
) {
911 records
= inlineRecords
[i
].value
.split(',');
912 childObjectId
= this.data
.map
[inlineRecords
[i
].name
];
913 childTable
= this.data
.config
[childObjectId
].table
;
914 for (j
= records
.length
- 1; j
>= 0; j
--) {
915 removeStack
.push(this.prependFormFieldNames
+ '[' + childTable
+ '][' + records
[j
] + ']');
919 removeStack
.push(this.prependFormFieldNames
+ shortName
);
920 TBE_EDITOR
.removeElementArray(removeStack
);
923 // Mark this container as deleted
924 var $deletedRecordContainer
= TYPO3
.jQuery('#' + this.escapeObjectId(objectId
) + '_div');
925 if ($deletedRecordContainer
.length
) {
926 $deletedRecordContainer
.addClass('inlineIsDeletedRecord');
929 // If the record is new and was never saved before, just remove it from DOM:
930 if (this.isNewRecord(objectId
) || options
&& options
.forceDirectRemoval
) {
931 this.fadeAndRemove(objectId
+ '_div');
932 // If the record already exists in storage, mark it to be deleted on clicking the save button:
934 document
.getElementsByName('cmd' + shortName
+ '[delete]')[0].disabled
= false;
935 new Effect
.Fade(objectId
+ '_div');
938 var recordCount
= this.memorizeRemoveRecord(
939 this.prependFormFieldNames
+ this.parseObjectId('parts', objectId
, 3, 2, true),
943 if (recordCount
<= 1) {
944 this.destroyDragAndDropSorting(this.parseObjectId('full', objectId
, 0, 2) + '_records');
946 this.redrawSortingButtons(objectPrefix
);
948 // if the NEW-button was hidden and now we can add again new children, show the button
949 if (!beforeDeleteIsBelowMax
&& this.isBelowMax(objectPrefix
)) {
950 var objectParent
= this.parseObjectId('full', objectPrefix
, 0, 1);
951 var md5
= this.getObjectMD5(objectParent
);
952 this.showElementsWithClassName('.inlineNewButton' + (md5
? '.' + md5
: ''), objectParent
);
957 parsePath: function (path
) {
958 var backSlash
= path
.lastIndexOf('\\');
959 var normalSlash
= path
.lastIndexOf('/');
962 path
= path
.substring(0, backSlash
+ 1);
963 } else if (normalSlash
> 0) {
964 path
= path
.substring(0, normalSlash
+ 1);
972 parseFormElementName: function (wrap
, formElementName
, rightCount
, skipRight
) {
973 var idParts
= this.splitFormElementName(formElementName
);
983 for (var i
= 0; i
< skipRight
; i
++) {
987 if (rightCount
> 0) {
988 for (var i
= 0; i
< rightCount
; i
++) {
989 elParts
.unshift(idParts
.pop());
992 for (var i
= 0; i
< -rightCount
; i
++) {
998 return this.constructFormElementName(wrap
, elParts
);
1001 splitFormElementName: function (formElementName
) {
1002 // remove left and right side "data[...|...]" -> '...|...'
1003 formElementName
= formElementName
.substr(0, formElementName
.lastIndexOf(']')).substr(formElementName
.indexOf('[') + 1);
1004 return formElementName
.split('][');
1007 splitObjectId: function (objectId
) {
1008 objectId
= objectId
.substr(objectId
.indexOf(this.structureSeparator
) + 1);
1009 objectId
= objectId
.split(this.flexFormSeparator
).join(this.flexFormSubstitute
);
1010 return objectId
.split(this.structureSeparator
);
1013 constructFormElementName: function (wrap
, parts
) {
1016 if (wrap
== 'full') {
1017 elReturn
= this.prependFormFieldNames
+ '[' + parts
.join('][') + ']';
1018 elReturn
= elReturn
.split(this.flexFormSubstitute
).join('][');
1019 } else if (wrap
== 'parts') {
1020 elReturn
= '[' + parts
.join('][') + ']';
1021 elReturn
= elReturn
.split(this.flexFormSubstitute
).join('][');
1022 } else if (wrap
== 'none') {
1023 elReturn
= parts
.length
> 1 ? parts
: parts
.join('');
1029 constructObjectId: function (wrap
, parts
) {
1032 if (wrap
== 'full') {
1033 elReturn
= this.prependFormFieldNames
+ this.structureSeparator
+ parts
.join(this.structureSeparator
);
1034 elReturn
= elReturn
.split(this.flexFormSubstitute
).join(this.flexFormSeparator
);
1035 } else if (wrap
== 'parts') {
1036 elReturn
= this.structureSeparator
+ parts
.join(this.structureSeparator
);
1037 elReturn
= elReturn
.split(this.flexFormSubstitute
).join(this.flexFormSeparator
);
1038 } else if (wrap
== 'none') {
1039 elReturn
= parts
.length
> 1 ? parts
: parts
.join('');
1045 parseObjectId: function (wrap
, objectId
, rightCount
, skipRight
, returnAsFormElementName
) {
1046 var idParts
= this.splitObjectId(objectId
);
1056 for (var i
= 0; i
< skipRight
; i
++) {
1060 if (rightCount
> 0) {
1061 for (var i
= 0; i
< rightCount
; i
++) {
1062 elParts
.unshift(idParts
.pop());
1065 for (var i
= 0; i
< -rightCount
; i
++) {
1072 if (returnAsFormElementName
) {
1073 elReturn
= this.constructFormElementName(wrap
, elParts
);
1075 elReturn
= this.constructObjectId(wrap
, elParts
);
1081 handleChangedField: function (formField
, objectId
) {
1083 if (typeof formField
== 'object') {
1084 formObj
= formField
;
1086 formObj
= document
.getElementsByName(formField
);
1087 if (formObj
.length
) {
1088 formObj
= formObj
[0];
1092 if (formObj
!= undefined) {
1094 if (formObj
.nodeName
== 'SELECT') {
1095 value
= formObj
.options
[formObj
.selectedIndex
].text
;
1097 value
= formObj
.value
;
1099 TYPO3
.jQuery('#' + this.escapeObjectId(objectId
) + '_label').html(value
.length
? value
: this.noTitleString
);
1104 arrayAssocCount: function (object
) {
1106 if (typeof object
.length
!= 'undefined') {
1107 count
= object
.length
;
1109 for (var i
in object
) {
1116 isBelowMax: function (objectPrefix
) {
1117 var isBelowMax
= true;
1118 var objectName
= this.prependFormFieldNames
+ this.parseObjectId('parts', objectPrefix
, 3, 1, true);
1119 var formObj
= document
.getElementsByName(objectName
);
1121 if (this.data
.config
&& this.data
.config
[objectPrefix
] && formObj
.length
) {
1122 var recordCount
= formObj
[0].value
? formObj
[0].value
.split(',').length
: 0;
1123 if (recordCount
>= this.data
.config
[objectPrefix
].max
) {
1127 if (isBelowMax
&& this.data
.unique
&& this.data
.unique
[objectPrefix
]) {
1128 var unique
= this.data
.unique
[objectPrefix
];
1129 if (this.arrayAssocCount(unique
.used
) >= unique
.max
&& unique
.max
>= 0) {
1136 getOptionsHash: function ($selectObj
) {
1137 var optionsHash
= {};
1138 $selectObj
.find('option').each(function(i
, option
) {
1139 optionsHash
[option
.value
] = i
;
1144 removeSelectOption: function ($selectObj
, value
) {
1145 var optionsHash
= this.getOptionsHash($selectObj
);
1146 if (optionsHash
[value
] != undefined) {
1147 $selectObj
.find('option').eq(optionsHash
[value
]).remove();
1151 readdSelectOption: function ($selectObj
, value
, unique
) {
1153 var optionsHash
= this.getOptionsHash($selectObj
);
1154 var possibleValues
= this.getKeysFromHashMap(unique
.possible
);
1156 for (var possibleValue
in unique
.possible
) {
1157 if (possibleValue
== value
) {
1160 if (optionsHash
[possibleValue
] != undefined) {
1161 index
= optionsHash
[possibleValue
];
1165 if (index
== null) {
1167 } else if (index
< $selectObj
.find('option').length
) {
1170 // recreate the <option> tag
1171 var readdOption
= document
.createElement('option');
1172 readdOption
.text
= unique
.possible
[value
];
1173 readdOption
.value
= value
;
1174 // add the <option> at the right position
1175 // I didn't find a possibility to add an option to a predefined position
1176 // with help of an index in jQuery. So we realized it the "old" style
1177 var selectObj
= $selectObj
.get(0);
1178 selectObj
.add(readdOption
, document
.all
? index
: selectObj
.options
[index
]);
1181 hideElementsWithClassName: function (selector
, parentElement
) {
1182 this.setVisibilityOfElementsWithClassName('hide', selector
, parentElement
);
1185 showElementsWithClassName: function (selector
, parentElement
) {
1186 this.setVisibilityOfElementsWithClassName('show', selector
, parentElement
);
1189 setVisibilityOfElementsWithClassName: function (action
, selector
, parentElement
) {
1190 var domObjects
= Selector
.findChildElements($(parentElement
), [selector
]);
1191 if (action
== 'hide') {
1192 TYPO3
.jQuery
.each(domObjects
, function (index
, domObject
) {
1193 new Effect
.Fade(domObject
);
1195 } else if (action
== 'show') {
1196 TYPO3
.jQuery
.each(domObjects
, function (index
, domObject
) {
1197 new Effect
.Appear(domObject
);
1202 fadeOutFadeIn: function (objectId
) {
1203 var optIn
= { duration
: 0.5, transition
: Effect
.Transitions
.linear
, from: 0.50, to
: 1.00 };
1204 var optOut
= { duration
: 0.5, transition
: Effect
.Transitions
.linear
, from: 1.00, to
: 0.50 };
1205 optOut
.afterFinish = function () {
1206 new Effect
.Opacity(objectId
, optIn
);
1208 new Effect
.Opacity(objectId
, optOut
);
1211 isNewRecord: function (objectId
) {
1212 var $selector
= TYPO3
.jQuery('#' + this.escapeObjectId(objectId
) + '_div');
1213 return $selector
.length
&& $selector
.hasClass('inlineIsNewRecord')
1218 // Find and fix nested of inline and tab levels if a new element was created dynamically (it doesn't know about its nesting):
1219 findContinuedNestedLevel: function (nested
, objectId
) {
1220 if (this.data
.nested
&& this.data
.nested
[objectId
]) {
1221 // Remove the first element from the new nested stack, it's just a hint:
1223 nested
= this.data
.nested
[objectId
].concat(nested
);
1230 getNumberOfRTE: function () {
1232 if (typeof RTEarea
!= 'undefined' && RTEarea
.length
> 0) {
1233 number
= RTEarea
.length
- 1;
1238 getObjectMD5: function (objectPrefix
) {
1240 if (this.data
.config
&& this.data
.config
[objectPrefix
] && this.data
.config
[objectPrefix
].md5
) {
1241 md5
= this.data
.config
[objectPrefix
].md5
;
1246 fadeAndRemove: function (element
) {
1247 if (TYPO3
.jQuery('#' + this.escapeObjectId(element
)).length
) {
1248 new Effect
.Fade(element
, { afterFinish: function () {
1249 Element
.remove(element
);
1254 getContext: function (objectId
) {
1257 if (objectId
!== '' && typeof this.data
.config
[objectId
] !== 'undefined' && typeof this.data
.config
[objectId
].context
!== 'undefined') {
1258 result
= this.data
.config
[objectId
].context
;
1265 * Escapes object identifiers to be used in jQuery.
1267 * @param string objectId
1270 escapeObjectId: function (objectId
) {
1271 var escapedObjectId
;
1272 escapedObjectId
= objectId
.replace(/:/g
, '\\:');
1273 escapedObjectId
= escapedObjectId
.replace(/\./g, '\\.');
1274 return escapedObjectId
;
1278 * Escapes object identifiers to be used as jQuery selector.
1280 * @param string objectId
1283 escapeSelectorObjectId: function (objectId
) {
1284 var escapedSelectorObjectId
;
1285 var escapedObjectId
= this.escapeObjectId(objectId
);
1286 escapedSelectorObjectId
= escapedObjectId
.replace(/\\:/g, '\\\\\\:');
1287 escapedSelectorObjectId
= escapedSelectorObjectId
.replace(/\\\./g, '\\\\\\.');
1288 return escapedSelectorObjectId
;
1292 Object
.extend(Array
.prototype, {
1293 diff: function (current
) {
1295 if (this.length
== current
.length
) {
1296 for (var i
= 0; i
< this.length
; i
++) {
1297 if (this[i
] !== current
[i
]) {
1309 $(document
).delegate('div.t3-form-field-header-inline', 'click', inline
.toggleEvent
);