b296c8452ca0b1e0843d503f916fa215288d89a9
[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 V20 H15',
67 vertical: 'M0 0 V20 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,
210 listItem, listItemContent, searchElement;
211 assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478715704);
212
213 isLastFormElementWithinParentFormElement = false;
214 if (formElement.get('__identifierPath') === getFormEditorApp().getLastFormElementWithinParentFormElement(formElement).get('__identifierPath')) {
215 isLastFormElementWithinParentFormElement = true;
216 }
217
218 listItem = $('<li></li>');
219 if (!getFormElementDefinition(formElement, '_isCompositeFormElement')) {
220 listItem.addClass(getHelper().getDomElementClassName('noNesting'));
221 }
222
223 listItemContent = $('<div></div>')
224 .attr(getHelper().getDomElementDataAttribute('elementIdentifier'), formElement.get('__identifierPath'))
225 .append(
226 $('<span></span>')
227 .attr(getHelper().getDomElementDataAttribute('identifier'), getHelper().getDomElementDataAttributeValue('title'))
228 .html(buildTitleByFormElement(formElement))
229 );
230
231 if (getFormElementDefinition(formElement, '_isCompositeFormElement')) {
232 listItemContent.attr(getHelper().getDomElementDataAttribute('abstractType'), 'isCompositeFormElement');
233 }
234 if (getFormElementDefinition(formElement, '_isTopLevelFormElement')) {
235 listItemContent.attr(getHelper().getDomElementDataAttribute('abstractType'), 'isTopLevelFormElement');
236 }
237
238 expanderItem = $('<span></span>').attr('data-identifier', getHelper().getDomElementDataAttributeValue('expander'));
239 listItemContent.prepend(expanderItem);
240
241 Icons.getIcon(getFormElementDefinition(formElement, 'iconIdentifier'), Icons.sizes.small, null, Icons.states.default).done(function(icon) {
242 expanderItem.after(
243 $(icon).addClass(getHelper().getDomElementClassName('icon'))
244 .tooltip({
245 title: 'identifier: ' + formElement.get('identifier'),
246 placement: 'right'
247 })
248 );
249
250 if (getFormElementDefinition(formElement, '_isCompositeFormElement')) {
251 if (formElement.get('renderables') && formElement.get('renderables').length > 0) {
252 Icons.getIcon(getHelper().getDomElementDataAttributeValue('collapse'), Icons.sizes.small).done(function(icon) {
253 expanderItem.before(_getLinkSvg('angle')).html($(icon));
254 listItem.addClass(getHelper().getDomElementClassName('hasChildren'));
255 });
256 } else {
257 expanderItem.before(_getLinkSvg('angle')).remove();
258 }
259 } else {
260 listItemContent.prepend(_getLinkSvg('angle'));
261 expanderItem.remove();
262 }
263
264 searchElement = formElement.get('__parentRenderable');
265 while (searchElement) {
266 if (searchElement.get('__identifierPath') === getRootFormElement().get('__identifierPath')) {
267 break;
268 }
269
270 if (searchElement.get('__identifierPath') === getFormEditorApp().getLastFormElementWithinParentFormElement(searchElement).get('__identifierPath')) {
271 listItemContent.prepend(_getLinkSvg('hidden'));
272 } else {
273 listItemContent.prepend(_getLinkSvg('vertical'));
274 }
275 searchElement = searchElement.get('__parentRenderable');
276 }
277 });
278 listItem.append(listItemContent);
279
280 getPublisherSubscriber().publish('view/tree/render/listItemAdded', [listItem, formElement]);
281 childFormElements = formElement.get('renderables');
282 childList = null;
283 if ('array' === $.type(childFormElements)) {
284 childList = $('<ol></ol>');
285 for (var i = 0, len = childFormElements.length; i < len; ++i) {
286 childList.append(_renderNestedSortableListItem(childFormElements[i]));
287 }
288 }
289
290 if (childList) {
291 listItem.append(childList);
292 }
293 return listItem;
294 };
295
296 /**
297 * @private
298 *
299 * @return void
300 * @publish view/tree/dnd/stop
301 * @publish view/tree/dnd/change
302 * @publish view/tree/dnd/update
303 */
304 function _addSortableEvents() {
305 $('ol.' + getHelper().getDomElementClassName('sortable'), _treeDomElement).nestedSortable({
306 forcePlaceholderSize: true,
307 protectRoot: true,
308 isTree: true,
309 handle: 'div' + getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'),
310 helper: 'clone',
311 items: 'li',
312 opacity: .6,
313 revert: 250,
314 delay: 200,
315 tolerance: 'pointer',
316 toleranceElement: '> div',
317
318 isAllowed: function(placeholder, placeholderParent, currentItem) {
319 var formElementIdentifierPath, formElementTypeDefinition, targetFormElementIdentifierPath,
320 targetFormElementTypeDefinition;
321
322 if (typeof placeholderParent === 'undefined') {
323 return true;
324 }
325
326 formElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(currentItem));
327 targetFormElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(placeholderParent));
328
329 formElementTypeDefinition = getFormElementDefinition(formElementIdentifierPath);
330 targetFormElementTypeDefinition = getFormElementDefinition(targetFormElementIdentifierPath);
331
332 if (
333 targetFormElementTypeDefinition['_isTopLevelFormElement']
334 && !targetFormElementTypeDefinition['_isCompositeFormElement']
335 ) {
336 return false;
337 }
338
339 if (
340 formElementTypeDefinition['_isGridContainerFormElement']
341 && (
342 getFormEditorApp().findEnclosingGridContainerFormElement(targetFormElementIdentifierPath)
343 || getFormEditorApp().findEnclosingGridRowFormElement(targetFormElementIdentifierPath)
344 )
345 ) {
346 return false;
347 }
348
349 if (
350 !formElementTypeDefinition['_isGridContainerFormElement']
351 && !formElementTypeDefinition['_isGridRowFormElement']
352 && targetFormElementTypeDefinition['_isGridContainerFormElement']
353 ) {
354 return false;
355 }
356
357 return true;
358 },
359 stop: function(e, o) {
360 getPublisherSubscriber().publish('view/tree/dnd/stop', [getTreeNodeIdentifierPathWithinDomElement($(o.item))]);
361 },
362 change: function(e, o) {
363 var enclosingCompositeFormElement, parentFormElementIdentifierPath;
364
365 parentFormElementIdentifierPath = getParentTreeNodeIdentifierPathWithinDomElement($(o.placeholder));
366 if (parentFormElementIdentifierPath) {
367 enclosingCompositeFormElement = getFormEditorApp().findEnclosingCompositeFormElementWhichIsNotOnTopLevel(parentFormElementIdentifierPath);
368 }
369 getPublisherSubscriber().publish('view/tree/dnd/change', [$(o.placeholder), parentFormElementIdentifierPath, enclosingCompositeFormElement]);
370 },
371 update: function(e, o) {
372 var nextFormElementIdentifierPath, movedFormElementIdentifierPath,
373 previousFormElementIdentifierPath;
374
375 movedFormElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(o.item));
376 previousFormElementIdentifierPath = getSiblingTreeNodeIdentifierPathWithinDomElement($(o.item), 'prev');
377 nextFormElementIdentifierPath = getSiblingTreeNodeIdentifierPathWithinDomElement($(o.item), 'next');
378
379 getPublisherSubscriber().publish('view/tree/dnd/update', [$(o.item), movedFormElementIdentifierPath, previousFormElementIdentifierPath, nextFormElementIdentifierPath]);
380 }
381 });
382 };
383
384 /**
385 * @private
386 *
387 * @return void
388 */
389 function _saveExpanderStates() {
390 var addStates;
391
392 addStates = function(formElement) {
393 var childFormElements, treeNode;
394
395 if (getFormElementDefinition(formElement, '_isCompositeFormElement')) {
396 treeNode = getTreeNode(formElement);
397 if (treeNode.length) {
398 if (treeNode.closest('li').hasClass(getHelper().getDomElementClassName('expanded'))) {
399 _expanderStates[formElement.get('__identifierPath')] = true;
400 } else {
401 _expanderStates[formElement.get('__identifierPath')] = false;
402 }
403 }
404
405 if (getUtility().isUndefinedOrNull(_expanderStates[formElement.get('__identifierPath')])) {
406 _expanderStates[formElement.get('__identifierPath')] = true;
407 }
408 }
409
410 childFormElements = formElement.get('renderables');
411 if ('array' === $.type(childFormElements)) {
412 for (var i = 0, len = childFormElements.length; i < len; ++i) {
413 addStates(childFormElements[i]);
414 }
415 }
416 };
417 addStates(getRootFormElement());
418
419 for (var identifierPath in _expanderStates) {
420 if (!_expanderStates.hasOwnProperty(identifierPath)) {
421 continue;
422 }
423 try {
424 getFormEditorApp().getFormElementByIdentifierPath(identifierPath);
425 } catch (error) {
426 delete _expanderStates[identifierPath];
427 }
428 }
429 };
430
431 /**
432 * @private
433 *
434 * @return void
435 */
436 function _loadExpanderStates() {
437 for (var identifierPath in _expanderStates) {
438 var treeNode;
439
440 if (!_expanderStates.hasOwnProperty(identifierPath)) {
441 continue;
442 }
443 treeNode = getTreeNode(identifierPath);
444 if (treeNode.length) {
445 if (_expanderStates[identifierPath]) {
446 treeNode.closest('li')
447 .removeClass(getHelper().getDomElementClassName('collapsed'))
448 .addClass(getHelper().getDomElementClassName('expanded'));
449 } else {
450 treeNode.closest('li')
451 .addClass(getHelper().getDomElementClassName('collapsed'))
452 .removeClass(getHelper().getDomElementClassName('expanded'));
453 }
454 }
455 }
456 };
457
458 /* *************************************************************
459 * Public Methodes
460 * ************************************************************/
461
462 /**
463 * @public
464 *
465 * @param object
466 * @return object
467 * @throws 1478721208
468 */
469 function renderCompositeFormElementChildsAsSortableList(formElement) {
470 var elementList;
471 assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478721208);
472
473 elementList = $('<ol></ol>').addClass(getHelper().getDomElementClassName('sortable'));
474 if ('array' === $.type(formElement.get('renderables'))) {
475 for (var i = 0, len = formElement.get('renderables').length; i < len; ++i) {
476 elementList.append(_renderNestedSortableListItem(formElement.get('renderables')[i]));
477 }
478 }
479 return elementList;
480 };
481
482 /**
483 * @public
484 *
485 * @return void
486 * @param object
487 * @publish view/tree/node/clicked
488 */
489 function renew(formElement) {
490 if (getFormEditorApp().getUtility().isUndefinedOrNull(formElement)) {
491 formElement = getRootFormElement();
492 }
493 _saveExpanderStates();
494 _treeDomElement.off().empty().append(renderCompositeFormElementChildsAsSortableList(formElement));
495
496 _treeDomElement.on("click", function(e) {
497 var formElementIdentifierPath;
498
499 formElementIdentifierPath = $(e.target)
500 .closest(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'))
501 .attr(getHelper().getDomElementDataAttribute('elementIdentifier'));
502 if (getUtility().isUndefinedOrNull(formElementIdentifierPath) || !getUtility().isNonEmptyString(formElementIdentifierPath)) {
503 return;
504 }
505 getPublisherSubscriber().publish('view/tree/node/clicked', [formElementIdentifierPath]);
506 });
507
508 $(getHelper().getDomElementDataIdentifierSelector('expander'), _treeDomElement).on('click', function() {
509 $(this).closest('li').toggleClass(getHelper().getDomElementClassName('collapsed')).toggleClass(getHelper().getDomElementClassName('expanded'));
510 });
511
512 if (_configuration['isSortable']) {
513 _addSortableEvents();
514 }
515 _loadExpanderStates();
516 };
517
518 /**
519 * @public
520 *
521 * @param object
522 * @return string
523 */
524 function getAllTreeNodes() {
525 return $(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'), _treeDomElement);
526 };
527
528 /**
529 * @public
530 *
531 * @param object
532 * @return string
533 */
534 function getTreeNodeWithinDomElement(element) {
535 return $(element).find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')).first();
536 };
537
538 /**
539 * @public
540 *
541 * @param object
542 * @return string
543 */
544 function getTreeNodeIdentifierPathWithinDomElement(element) {
545 return getTreeNodeWithinDomElement($(element)).attr(getHelper().getDomElementDataAttribute('elementIdentifier'));
546 };
547
548 /**
549 * @public
550 *
551 * @param object
552 * @return string
553 */
554 function getParentTreeNodeWithinDomElement(element) {
555 return $(element).parent().closest('li').find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')).first();
556 };
557
558 /**
559 * @public
560 *
561 * @param object
562 * @return string
563 */
564 function getParentTreeNodeIdentifierPathWithinDomElement(element) {
565 return getParentTreeNodeWithinDomElement(element).attr(getHelper().getDomElementDataAttribute('elementIdentifier'));
566 };
567
568 /**
569 * @private
570 *
571 * @param object
572 * @param string
573 * @return string
574 */
575 function getSiblingTreeNodeIdentifierPathWithinDomElement(element, position) {
576 var formElementIdentifierPath;
577
578 if (getUtility().isUndefinedOrNull(position)) {
579 position = 'prev';
580 }
581 formElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement(element);
582 element = (position === 'prev') ? $(element).prev('li') : $(element).next('li');
583 return element.find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'))
584 .not(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKeyValue', [formElementIdentifierPath]))
585 .first()
586 .attr(getHelper().getDomElementDataAttribute('elementIdentifier'));
587 };
588
589 /**
590 * @public
591 *
592 * @param string
593 * @param object
594 * @return void
595 */
596 function setTreeNodeTitle(title, formElement) {
597 if (getUtility().isUndefinedOrNull(title)) {
598 title = buildTitleByFormElement(formElement);
599 }
600
601 $(getHelper().getDomElementDataIdentifierSelector('title'), getTreeNode(formElement)).html(title);
602 };
603
604 /**
605 * @public
606 *
607 * @param string|object
608 * @return object
609 */
610 function getTreeNode(formElement) {
611 var formElementIdentifierPath;
612
613 if ('string' === $.type(formElement)) {
614 formElementIdentifierPath = formElement;
615 } else {
616 if (getUtility().isUndefinedOrNull(formElement)) {
617 formElementIdentifierPath = getCurrentlySelectedFormElement().get('__identifierPath');
618 } else {
619 formElementIdentifierPath = formElement.get('__identifierPath');
620 }
621 }
622 return $(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKeyValue', [formElementIdentifierPath]), _treeDomElement);
623 };
624
625 /**
626 * @public
627 *
628 * @param object
629 * @return object
630 * @throws 1478719287
631 */
632 function buildTitleByFormElement(formElement) {
633 if (getUtility().isUndefinedOrNull(formElement)) {
634 formElement = getCurrentlySelectedFormElement();
635 }
636 assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478719287);
637
638 return $('<span></span>')
639 .text((formElement.get('label') ? formElement.get('label') : formElement.get('identifier')))
640 .append($('<small></small>').text("(" + getFormElementDefinition(formElement, 'label') + ")"));
641 };
642
643 /**
644 * @public
645 *
646 * @return object
647 */
648 function getTreeDomElement() {
649 return _treeDomElement;
650 };
651
652 /**
653 * @public
654 *
655 * @param object
656 * @param object
657 * @param object
658 * @return this
659 * @throws 1478714814
660 */
661 function bootstrap(formEditorApp, appendToDomElement, configuration) {
662 _formEditorApp = formEditorApp;
663 assert('object' === $.type(appendToDomElement), 'Invalid parameter "appendToDomElement"', 1478714814);
664
665 _treeDomElement = $(appendToDomElement);
666 _configuration = $.extend(true, _defaultConfiguration, configuration || {});
667 _helperSetup();
668 return this;
669 };
670
671 /**
672 * Publish the public methods.
673 * Implements the "Revealing Module Pattern".
674 */
675 return {
676 bootstrap: bootstrap,
677 buildTitleByFormElement: buildTitleByFormElement,
678 getAllTreeNodes: getAllTreeNodes,
679 getParentTreeNodeWithinDomElement: getParentTreeNodeWithinDomElement,
680 getParentTreeNodeIdentifierPathWithinDomElement: getParentTreeNodeIdentifierPathWithinDomElement,
681 getSiblingTreeNodeIdentifierPathWithinDomElement: getSiblingTreeNodeIdentifierPathWithinDomElement,
682 getTreeDomElement: getTreeDomElement,
683 getTreeNode: getTreeNode,
684 getTreeNodeWithinDomElement: getTreeNodeWithinDomElement,
685 getTreeNodeIdentifierPathWithinDomElement: getTreeNodeIdentifierPathWithinDomElement,
686 renderCompositeFormElementChildsAsSortableList: renderCompositeFormElementChildsAsSortableList,
687 renew: renew,
688 setTreeNodeTitle: setTreeNodeTitle
689 };
690 })($, Helper, Icons);
691 });