03a45127fc9509c0417ed09d93d649b26381f73d
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Resources / Public / JavaScript / Backend / FormEditor / TreeComponent.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13
14 /**
15 * Module: TYPO3/CMS/Form/Backend/FormEditor/TreeComponent
16 */
17 define(['jquery',
18 'TYPO3/CMS/Form/Backend/FormEditor/Helper',
19 'TYPO3/CMS/Backend/Icons',
20 'TYPO3/CMS/Form/Backend/Vendor/jquery.mjs.nestedSortable'
21 ], function($, Helper, Icons) {
22 'use strict';
23
24 return (function($, Helper, Icons) {
25
26 /**
27 * @private
28 *
29 * @var object
30 */
31 var _configuration = null;
32
33 /**
34 * @private
35 *
36 * @var object
37 */
38 var _expanderStates = {};
39
40 /**
41 * @private
42 *
43 * @var object
44 */
45 var _defaultConfiguration = {
46 domElementClassNames: {
47 collapsed: 'mjs-nestedSortable-collapsed',
48 expanded: 'mjs-nestedSortable-expanded',
49 hasChildren: 't3-form-element-has-children',
50 sortable: 'sortable',
51 svgLinkWrapper: 'svg-wrapper',
52 noNesting: 'mjs-nestedSortable-no-nesting'
53 },
54 domElementDataAttributeNames: {
55 abstractType: 'data-element-abstract-type'
56 },
57 domElementDataAttributeValues: {
58 collapse: 'actions-pagetree-collapse',
59 expander: 'treeExpander',
60 title: 'treeTitle'
61 },
62 isSortable: true,
63 svgLink: {
64 height: 15,
65 paths: {
66 angle: 'M0 0 V21 H15',
67 vertical: 'M0 0 V21 H0',
68 hidden: 'M0 0 V0 H0'
69 },
70 width: 15
71 }
72 };
73
74 /**
75 * @private
76 *
77 * @var object
78 */
79 var _formEditorApp = null;
80
81 /**
82 * @private
83 *
84 * @var object
85 */
86 var _treeDomElement = null;
87
88 /* *************************************************************
89 * Private Methodes
90 * ************************************************************/
91
92 /**
93 * @private
94 *
95 * @return void
96 * @throws 1478268638
97 */
98 function _helperSetup() {
99 assert('function' === $.type(Helper.bootstrap),
100 'The view model helper does not implement the method "bootstrap"',
101 1478268638
102 );
103 Helper.bootstrap(getFormEditorApp());
104 };
105
106 /**
107 * @private
108 *
109 * @return object
110 */
111 function getFormEditorApp() {
112 return _formEditorApp;
113 };
114
115 /**
116 * @public
117 *
118 * @param object
119 * @return object
120 */
121 function getHelper(configuration) {
122 if (getUtility().isUndefinedOrNull(configuration)) {
123 return Helper.setConfiguration(_configuration);
124 }
125 return Helper.setConfiguration(configuration);
126 };
127
128 /**
129 * @private
130 *
131 * @return object
132 */
133 function getUtility() {
134 return getFormEditorApp().getUtility();
135 };
136
137 /**
138 * @private
139 *
140 * @param mixed test
141 * @param string message
142 * @param int messageCode
143 * @return void
144 */
145 function assert(test, message, messageCode) {
146 return getFormEditorApp().assert(test, message, messageCode);
147 };
148
149 /**
150 * @private
151 *
152 * @return object
153 */
154 function getRootFormElement() {
155 return getFormEditorApp().getRootFormElement();
156 };
157
158 /**
159 * @private
160 *
161 * @return object
162 */
163 function getCurrentlySelectedFormElement() {
164 return getFormEditorApp().getCurrentlySelectedFormElement();
165 };
166
167 /**
168 * @private
169 *
170 * @return object
171 */
172 function getPublisherSubscriber() {
173 return getFormEditorApp().getPublisherSubscriber();
174 };
175
176 /**
177 * @private
178 *
179 * @param object
180 * @param string
181 * @return mixed
182 */
183 function getFormElementDefinition(formElement, formElementDefinitionKey) {
184 return getFormEditorApp().getFormElementDefinition(formElement, formElementDefinitionKey);
185 };
186
187 /**
188 * @private
189 *
190 * @return object
191 */
192 function _getLinkSvg(type) {
193 return $('<span class="' + getHelper().getDomElementClassName('svgLinkWrapper') + '">'
194 + '<svg version="1.1" width="' + _configuration['svgLink']['width'] + '" height="' + _configuration['svgLink']['height'] + '">'
195 + '<path class="link" d="' + _configuration['svgLink']['paths'][type] + '">'
196 + '</svg>'
197 + '</span>');
198 };
199
200 /**
201 * @private
202 *
203 * @param object
204 * @return object
205 * @publish view/tree/render/listItemAdded
206 * @throws 1478715704
207 */
208 function _renderNestedSortableListItem(formElement) {
209 var childFormElements, childList, expanderItem, isLastFormElementWithinParentFormElement, listItem, listItemContent, searchElement;
210 assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478715704);
211
212 isLastFormElementWithinParentFormElement = false;
213 if (formElement.get('__identifierPath') === getFormEditorApp().getLastFormElementWithinParentFormElement(formElement).get('__identifierPath')) {
214 isLastFormElementWithinParentFormElement = true;
215 }
216
217 listItem = $('<li></li>');
218 if (!getFormElementDefinition(formElement, '_isCompositeFormElement')) {
219 listItem.addClass(getHelper().getDomElementClassName('noNesting'));
220 }
221
222 listItemContent = $('<div></div>')
223 .attr(getHelper().getDomElementDataAttribute('elementIdentifier'), formElement.get('__identifierPath'))
224 .append(
225 $('<span></span>')
226 .attr(getHelper().getDomElementDataAttribute('identifier'), getHelper().getDomElementDataAttributeValue('title'))
227 .html(buildTitleByFormElement(formElement))
228 );
229
230 if (getFormElementDefinition(formElement, '_isCompositeFormElement')) {
231 listItemContent.attr(getHelper().getDomElementDataAttribute('abstractType'), 'isCompositeFormElement');
232 }
233 if (getFormElementDefinition(formElement, '_isTopLevelFormElement')) {
234 listItemContent.attr(getHelper().getDomElementDataAttribute('abstractType'), 'isTopLevelFormElement');
235 }
236
237 expanderItem = $('<span></span>').attr('data-identifier', getHelper().getDomElementDataAttributeValue('expander'));
238 listItemContent.prepend(expanderItem);
239
240 Icons.getIcon(getFormElementDefinition(formElement, 'iconIdentifier'), Icons.sizes.small, null, Icons.states.default).done(function(icon) {
241 expanderItem.after(
242 $(icon).addClass(getHelper().getDomElementClassName('icon'))
243 .tooltip({
244 title: 'identifier: ' + formElement.get('identifier'),
245 placement: 'right'
246 })
247 );
248
249 if (getFormElementDefinition(formElement, '_isCompositeFormElement')) {
250 if (formElement.get('renderables') && formElement.get('renderables').length > 0) {
251 Icons.getIcon(getHelper().getDomElementDataAttributeValue('collapse'), Icons.sizes.small).done(function(icon) {
252 expanderItem.before(_getLinkSvg('angle')).html($(icon));
253 listItem.addClass(getHelper().getDomElementClassName('hasChildren'));
254 });
255 } else {
256 expanderItem.before(_getLinkSvg('angle')).remove();
257 }
258 } else {
259 listItemContent.prepend(_getLinkSvg('angle'));
260 expanderItem.remove();
261 }
262
263 searchElement = formElement.get('__parentRenderable');
264 while (searchElement) {
265 if (searchElement.get('__identifierPath') === getRootFormElement().get('__identifierPath')) {
266 break;
267 }
268
269 if (searchElement.get('__identifierPath') === getFormEditorApp().getLastFormElementWithinParentFormElement(searchElement).get('__identifierPath')) {
270 listItemContent.prepend(_getLinkSvg('hidden'));
271 } else {
272 listItemContent.prepend(_getLinkSvg('vertical'));
273 }
274 searchElement = searchElement.get('__parentRenderable');
275 }
276 });
277 listItem.append(listItemContent);
278
279 getPublisherSubscriber().publish('view/tree/render/listItemAdded', [listItem, formElement]);
280 childFormElements = formElement.get('renderables');
281 childList = null;
282 if ('array' === $.type(childFormElements)) {
283 childList = $('<ol></ol>');
284 for (var i = 0, len = childFormElements.length; i < len; ++i) {
285 childList.append(_renderNestedSortableListItem(childFormElements[i]));
286 }
287 }
288
289 if (childList) {
290 listItem.append(childList);
291 }
292 return listItem;
293 };
294
295 /**
296 * @private
297 *
298 * @return void
299 * @publish view/tree/dnd/stop
300 * @publish view/tree/dnd/change
301 * @publish view/tree/dnd/update
302 */
303 function _addSortableEvents() {
304 $('ol.' + getHelper().getDomElementClassName('sortable'), _treeDomElement).nestedSortable({
305 forcePlaceholderSize: true,
306 protectRoot: true,
307 isTree: true,
308 handle: 'div' + getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'),
309 helper: 'clone',
310 items: 'li',
311 opacity: .6,
312 revert: 250,
313 delay: 200,
314 tolerance: 'pointer',
315 toleranceElement: '> div',
316
317 isAllowed: function (placeholder, placeholderParent, currentItem) {
318 var formElementIdentifierPath, formElementTypeDefinition, targetFormElementIdentifierPath, targetFormElementTypeDefinition;
319
320 formElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(currentItem));
321 targetFormElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(placeholderParent));
322
323 formElementTypeDefinition = getFormElementDefinition(formElementIdentifierPath);
324 targetFormElementTypeDefinition = getFormElementDefinition(targetFormElementIdentifierPath);
325
326 if (
327 formElementTypeDefinition['_isGridContainerFormElement']
328 && (
329 getFormEditorApp().findEnclosingGridContainerFormElement(targetFormElementIdentifierPath)
330 || getFormEditorApp().findEnclosingGridRowFormElement(targetFormElementIdentifierPath)
331 )
332 ) {
333 return false;
334 }
335
336 if (
337 !formElementTypeDefinition['_isGridContainerFormElement']
338 && !formElementTypeDefinition['_isGridRowFormElement']
339 && targetFormElementTypeDefinition['_isGridContainerFormElement']
340 ) {
341 return false;
342 }
343
344 return true;
345 },
346 stop: function(e, o) {
347 getPublisherSubscriber().publish('view/tree/dnd/stop', [getTreeNodeIdentifierPathWithinDomElement($(o.item))]);
348 },
349 change: function(e, o) {
350 var enclosingCompositeFormElement, parentFormElementIdentifierPath;
351
352 parentFormElementIdentifierPath = getParentTreeNodeIdentifierPathWithinDomElement($(o.placeholder));
353 if (parentFormElementIdentifierPath) {
354 enclosingCompositeFormElement = getFormEditorApp().findEnclosingCompositeFormElementWhichIsNotOnTopLevel(parentFormElementIdentifierPath);
355 }
356 getPublisherSubscriber().publish('view/tree/dnd/change', [$(o.placeholder), parentFormElementIdentifierPath, enclosingCompositeFormElement]);
357 },
358 update: function(e, o) {
359 var nextFormElementIdentifierPath, movedFormElementIdentifierPath, previousFormElementIdentifierPath;
360
361 movedFormElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(o.item));
362 previousFormElementIdentifierPath = getSiblingTreeNodeIdentifierPathWithinDomElement($(o.item), 'prev');
363 nextFormElementIdentifierPath = getSiblingTreeNodeIdentifierPathWithinDomElement($(o.item), 'next');
364
365 getPublisherSubscriber().publish('view/tree/dnd/update', [$(o.item), movedFormElementIdentifierPath, previousFormElementIdentifierPath, nextFormElementIdentifierPath]);
366 }
367 });
368 };
369
370 /**
371 * @private
372 *
373 * @return void
374 */
375 function _saveExpanderStates() {
376 var addStates;
377
378 addStates = function(formElement) {
379 var childFormElements, treeNode;
380
381 if (getFormElementDefinition(formElement, '_isCompositeFormElement')) {
382 treeNode = getTreeNode(formElement);
383 if (treeNode.length) {
384 if (treeNode.closest('li').hasClass(getHelper().getDomElementClassName('expanded'))) {
385 _expanderStates[formElement.get('__identifierPath')] = true;
386 } else {
387 _expanderStates[formElement.get('__identifierPath')] = false;
388 }
389 }
390
391 if (getUtility().isUndefinedOrNull(_expanderStates[formElement.get('__identifierPath')])) {
392 _expanderStates[formElement.get('__identifierPath')] = true;
393 }
394 }
395
396 childFormElements = formElement.get('renderables');
397 if ('array' === $.type(childFormElements)) {
398 for (var i = 0, len = childFormElements.length; i < len; ++i) {
399 addStates(childFormElements[i]);
400 }
401 }
402 };
403 addStates(getRootFormElement());
404
405 for (var identifierPath in _expanderStates) {
406 if (!_expanderStates.hasOwnProperty(identifierPath)) {
407 continue;
408 }
409 try {
410 getFormEditorApp().getFormElementByIdentifierPath(identifierPath);
411 } catch(error) {
412 delete _expanderStates[identifierPath];
413 }
414 }
415 };
416
417 /**
418 * @private
419 *
420 * @return void
421 */
422 function _loadExpanderStates() {
423 for (var identifierPath in _expanderStates) {
424 var treeNode;
425
426 if (!_expanderStates.hasOwnProperty(identifierPath)) {
427 continue;
428 }
429 treeNode = getTreeNode(identifierPath);
430 if (treeNode.length) {
431 if (_expanderStates[identifierPath]) {
432 treeNode.closest('li')
433 .removeClass(getHelper().getDomElementClassName('collapsed'))
434 .addClass(getHelper().getDomElementClassName('expanded'));
435 } else {
436 treeNode.closest('li')
437 .addClass(getHelper().getDomElementClassName('collapsed'))
438 .removeClass(getHelper().getDomElementClassName('expanded'));
439 }
440 }
441 }
442 };
443
444 /* *************************************************************
445 * Public Methodes
446 * ************************************************************/
447
448 /**
449 * @public
450 *
451 * @param object
452 * @return object
453 * @throws 1478721208
454 */
455 function renderCompositeFormElementChildsAsSortableList(formElement) {
456 var elementList;
457 assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478721208);
458
459 elementList = $('<ol></ol>').addClass(getHelper().getDomElementClassName('sortable'));
460 if ('array' === $.type(formElement.get('renderables'))) {
461 for (var i = 0, len = formElement.get('renderables').length; i < len; ++i) {
462 elementList.append(_renderNestedSortableListItem(formElement.get('renderables')[i]));
463 }
464 }
465 return elementList;
466 };
467
468 /**
469 * @public
470 *
471 * @return void
472 * @param object
473 * @publish view/tree/node/clicked
474 */
475 function renew(formElement) {
476 if (getFormEditorApp().getUtility().isUndefinedOrNull(formElement)) {
477 formElement = getRootFormElement();
478 }
479 _saveExpanderStates();
480 _treeDomElement.off().empty().append(renderCompositeFormElementChildsAsSortableList(formElement));
481
482 _treeDomElement.on("click", function(e) {
483 var formElementIdentifierPath;
484
485 formElementIdentifierPath = $(e.target)
486 .closest(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'))
487 .attr(getHelper().getDomElementDataAttribute('elementIdentifier'));
488 if (getUtility().isUndefinedOrNull(formElementIdentifierPath) || !getUtility().isNonEmptyString(formElementIdentifierPath)) {
489 return;
490 }
491 getPublisherSubscriber().publish('view/tree/node/clicked', [formElementIdentifierPath]);
492 });
493
494 $(getHelper().getDomElementDataIdentifierSelector('expander'), _treeDomElement).on('click', function() {
495 $(this).closest('li').toggleClass(getHelper().getDomElementClassName('collapsed')).toggleClass(getHelper().getDomElementClassName('expanded'));
496 });
497
498 if (_configuration['isSortable']) {
499 _addSortableEvents();
500 }
501 _loadExpanderStates();
502 };
503
504 /**
505 * @public
506 *
507 * @param object
508 * @return string
509 */
510 function getAllTreeNodes() {
511 return $(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'), _treeDomElement);
512 };
513
514 /**
515 * @public
516 *
517 * @param object
518 * @return string
519 */
520 function getTreeNodeWithinDomElement(element) {
521 return $(element).find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')).first();
522 };
523
524 /**
525 * @public
526 *
527 * @param object
528 * @return string
529 */
530 function getTreeNodeIdentifierPathWithinDomElement(element) {
531 return getTreeNodeWithinDomElement($(element)).attr(getHelper().getDomElementDataAttribute('elementIdentifier'));
532 };
533
534 /**
535 * @public
536 *
537 * @param object
538 * @return string
539 */
540 function getParentTreeNodeWithinDomElement(element) {
541 return $(element).parent().closest('li').find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')).first();
542 };
543
544 /**
545 * @public
546 *
547 * @param object
548 * @return string
549 */
550 function getParentTreeNodeIdentifierPathWithinDomElement(element) {
551 return getParentTreeNodeWithinDomElement(element).attr(getHelper().getDomElementDataAttribute('elementIdentifier'));
552 };
553
554 /**
555 * @private
556 *
557 * @param object
558 * @param string
559 * @return string
560 */
561 function getSiblingTreeNodeIdentifierPathWithinDomElement(element, position) {
562 var formElementIdentifierPath;
563
564 if (getUtility().isUndefinedOrNull(position)) {
565 position = 'prev';
566 }
567 formElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement(element);
568 element = (position === 'prev') ? $(element).prev('li') : $(element).next('li');
569 return element.find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'))
570 .not(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKeyValue', [formElementIdentifierPath]))
571 .first()
572 .attr(getHelper().getDomElementDataAttribute('elementIdentifier'));
573 };
574
575 /**
576 * @public
577 *
578 * @param string
579 * @param object
580 * @return void
581 */
582 function setTreeNodeTitle(title, formElement) {
583 if (getUtility().isUndefinedOrNull(title)) {
584 title = buildTitleByFormElement(formElement);
585 }
586
587 $(getHelper().getDomElementDataIdentifierSelector('title'), getTreeNode(formElement)).html(title);
588 };
589
590 /**
591 * @public
592 *
593 * @param string|object
594 * @return object
595 */
596 function getTreeNode(formElement) {
597 var formElementIdentifierPath;
598
599 if ('string' === $.type(formElement)) {
600 formElementIdentifierPath = formElement;
601 } else {
602 if (getUtility().isUndefinedOrNull(formElement)) {
603 formElementIdentifierPath = getCurrentlySelectedFormElement().get('__identifierPath');
604 } else {
605 formElementIdentifierPath = formElement.get('__identifierPath');
606 }
607 }
608 return $(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKeyValue', [formElementIdentifierPath]), _treeDomElement);
609 };
610
611 /**
612 * @public
613 *
614 * @param object
615 * @return object
616 * @throws 1478719287
617 */
618 function buildTitleByFormElement(formElement) {
619 if (getUtility().isUndefinedOrNull(formElement)) {
620 formElement = getCurrentlySelectedFormElement();
621 }
622 assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478719287);
623
624 return $('<span></span>')
625 .text((formElement.get('label') ? formElement.get('label') : formElement.get('identifier')))
626 .append($('<small></small>').text("(" + getFormElementDefinition(formElement, 'label') + ")"));
627 };
628
629 /**
630 * @public
631 *
632 * @return object
633 */
634 function getTreeDomElement() {
635 return _treeDomElement;
636 };
637
638 /**
639 * @public
640 *
641 * @param object
642 * @param object
643 * @param object
644 * @return this
645 * @throws 1478714814
646 */
647 function bootstrap(formEditorApp, appendToDomElement, configuration) {
648 _formEditorApp = formEditorApp;
649 assert('object' === $.type(appendToDomElement), 'Invalid parameter "appendToDomElement"', 1478714814);
650
651 _treeDomElement = $(appendToDomElement);
652 _configuration = $.extend(true, _defaultConfiguration, configuration || {});
653 _helperSetup();
654 return this;
655 };
656
657 /**
658 * Publish the public methods.
659 * Implements the "Revealing Module Pattern".
660 */
661 return {
662 bootstrap: bootstrap,
663 buildTitleByFormElement: buildTitleByFormElement,
664 getAllTreeNodes: getAllTreeNodes,
665 getParentTreeNodeWithinDomElement: getParentTreeNodeWithinDomElement,
666 getParentTreeNodeIdentifierPathWithinDomElement: getParentTreeNodeIdentifierPathWithinDomElement,
667 getSiblingTreeNodeIdentifierPathWithinDomElement: getSiblingTreeNodeIdentifierPathWithinDomElement,
668 getTreeDomElement: getTreeDomElement,
669 getTreeNode: getTreeNode,
670 getTreeNodeWithinDomElement: getTreeNodeWithinDomElement,
671 getTreeNodeIdentifierPathWithinDomElement: getTreeNodeIdentifierPathWithinDomElement,
672 renderCompositeFormElementChildsAsSortableList: renderCompositeFormElementChildsAsSortableList,
673 renew: renew,
674 setTreeNodeTitle: setTreeNodeTitle
675 };
676 })($, Helper, Icons);
677 });