diff --git a/Build/Sources/TypeScript/form/backend/form-editor/tree-component.ts b/Build/Sources/TypeScript/form/backend/form-editor/tree-component.ts new file mode 100644 index 000000000000..93477fc42b4a --- /dev/null +++ b/Build/Sources/TypeScript/form/backend/form-editor/tree-component.ts @@ -0,0 +1,560 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * Module: @typo3/form/backend/form-editor/tree-component + */ +import $ from 'jquery'; +import * as Helper from '@typo3/form/backend/form-editor/helper'; +import Icons from '@typo3/backend/icons'; +import Sortable from 'sortablejs'; + +import type { + FormEditor, +} from '@typo3/form/backend/form-editor'; +import type { + Utility, + FormElement, + FormElementDefinition, + PublisherSubscriber, +} from '@typo3/form/backend/form-editor/core'; +import type { + Configuration as HelperConfiguration, +} from '@typo3/form/backend/form-editor/helper'; + +interface Configuration extends Partial { + isSortable: boolean, + svgLink: { + height: number, + width: number + paths: { + angle: string, + vertical: string, + hidden: string, + }, + } +} + +const defaultConfiguration: Configuration = { + domElementClassNames: { + collapsed: 'mjs-nestedSortable-collapsed', + expanded: 'mjs-nestedSortable-expanded', + hasChildren: 't3-form-element-has-children', + sortable: 'sortable', + svgLinkWrapper: 'svg-wrapper', + noNesting: 'mjs-nestedSortable-no-nesting' + }, + domElementDataAttributeNames: { + abstractType: 'data-element-abstract-type' + }, + domElementDataAttributeValues: { + collapse: 'actions-chevron-right', + expander: 'treeExpander', + title: 'treeTitle' + }, + isSortable: true, + svgLink: { + height: 15, + paths: { + angle: 'M0 0 V20 H15', + vertical: 'M0 0 V20 H0', + hidden: 'M0 0 V0 H0' + }, + width: 20 + } +}; + +let configuration: Configuration = null; + +let formEditorApp: FormEditor = null; + +let treeDomElement: JQuery = null; + +const expanderStates: Record = {}; + +function getFormEditorApp(): FormEditor { + return formEditorApp; +} + +function getHelper(_configuration?: HelperConfiguration): typeof Helper { + if (getUtility().isUndefinedOrNull(_configuration)) { + return Helper.setConfiguration(configuration); + } + return Helper.setConfiguration(_configuration); +} + +function getUtility(): Utility { + return getFormEditorApp().getUtility(); +} + +function assert(test: boolean|(() => boolean), message: string, messageCode: number): void { + return getFormEditorApp().assert(test, message, messageCode); +} + +function getRootFormElement(): FormElement { + return getFormEditorApp().getRootFormElement(); +} + +function getCurrentlySelectedFormElement(): FormElement { + return getFormEditorApp().getCurrentlySelectedFormElement(); +} + +function getPublisherSubscriber(): PublisherSubscriber { + return getFormEditorApp().getPublisherSubscriber(); +} + +function getFormElementDefinition( + formElement: FormElement, + formElementDefinitionKey?: T +): T extends keyof FormElementDefinition ? FormElementDefinition[T] : FormElementDefinition { + return getFormEditorApp().getFormElementDefinition(formElement, formElementDefinitionKey); +} + +function getLinkSvg(type: keyof Configuration['svgLink']['paths']): JQuery { + return $('' + + '' + + '' + + '' + + ''); +} + +/** + * @publish view/tree/render/listItemAdded + * @throws 1478715704 + */ +function renderNestedSortableListItem(formElement: FormElement): JQuery { + assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478715704); + + const listItem = $('
  • '); + if (!getFormElementDefinition(formElement, '_isCompositeFormElement')) { + listItem.addClass(getHelper().getDomElementClassName('noNesting')); + } + + const listItemContent = $('
    ') + .attr(getHelper().getDomElementDataAttribute('elementIdentifier'), formElement.get('__identifierPath')) + .append( + $('') + .attr(getHelper().getDomElementDataAttribute('identifier'), getHelper().getDomElementDataAttributeValue('title')) + .append(buildTitleByFormElement(formElement)) + ); + + if (getFormElementDefinition(formElement, '_isCompositeFormElement')) { + listItemContent.attr(getHelper().getDomElementDataAttribute('abstractType'), 'isCompositeFormElement'); + } + if (getFormElementDefinition(formElement, '_isTopLevelFormElement')) { + listItemContent.attr(getHelper().getDomElementDataAttribute('abstractType'), 'isTopLevelFormElement'); + } + + const expanderItem = $('').attr('data-identifier', getHelper().getDomElementDataAttributeValue('expander')); + listItemContent.prepend(expanderItem); + + Icons.getIcon(getFormElementDefinition(formElement, 'iconIdentifier'), Icons.sizes.small, null, Icons.states.default).then(function(icon) { + expanderItem.after( + $(icon).addClass(getHelper().getDomElementClassName('icon')) + .attr('title', 'id = ' + formElement.get('identifier')) + ); + + if (getFormElementDefinition(formElement, '_isCompositeFormElement')) { + if (formElement.get('renderables') && formElement.get('renderables').length > 0) { + Icons.getIcon(getHelper().getDomElementDataAttributeValue('collapse'), Icons.sizes.small).then(function(icon) { + expanderItem.before(getLinkSvg('angle')).html(icon); + listItem.addClass(getHelper().getDomElementClassName('hasChildren')); + }); + } else { + expanderItem.before(getLinkSvg('angle')).remove(); + } + } else { + listItemContent.prepend(getLinkSvg('angle')); + expanderItem.remove(); + } + + let searchElement = formElement.get('__parentRenderable'); + while (searchElement) { + if (searchElement.get('__identifierPath') === getRootFormElement().get('__identifierPath')) { + break; + } + + if (searchElement.get('__identifierPath') === getFormEditorApp().getLastFormElementWithinParentFormElement(searchElement).get('__identifierPath')) { + listItemContent.prepend(getLinkSvg('hidden')); + } else { + listItemContent.prepend(getLinkSvg('vertical')); + } + searchElement = searchElement.get('__parentRenderable'); + } + }); + listItem.append(listItemContent); + + getPublisherSubscriber().publish('view/tree/render/listItemAdded', [listItem, formElement]); + const childFormElements = formElement.get('renderables'); + let childList = null; + if ('array' === $.type(childFormElements)) { + childList = $('
      '); + for (let i = 0, len = childFormElements.length; i < len; ++i) { + childList.append(renderNestedSortableListItem(childFormElements[i])); + } + } + + if (childList) { + listItem.append(childList); + } + return listItem; +} + +/** + * @publish view/tree/dnd/stop + * @publish view/tree/dnd/change + * @publish view/tree/dnd/update + */ +function addSortableEvents(): void { + const defaultConfiguration: Sortable.Options = { + handle: 'div' + getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'), + draggable: 'li', + animation: 200, + fallbackTolerance: 200, + fallbackOnBody: true, + swapThreshold: 0.6, + dragClass: 'form-sortable-drag', + ghostClass: 'form-sortable-ghost', + onChange: function (e) { + let enclosingCompositeFormElement; + const parentFormElementIdentifierPath = getParentTreeNodeIdentifierPathWithinDomElement($(e.item)); + + if (parentFormElementIdentifierPath) { + enclosingCompositeFormElement = getFormEditorApp().findEnclosingCompositeFormElementWhichIsNotOnTopLevel(parentFormElementIdentifierPath); + } + getPublisherSubscriber().publish('view/tree/dnd/change', [$(e.item), parentFormElementIdentifierPath, enclosingCompositeFormElement]); + }, + onEnd: function (e) { + const movedFormElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(e.item)); + const previousFormElementIdentifierPath = getSiblingTreeNodeIdentifierPathWithinDomElement($(e.item), 'prev'); + const nextFormElementIdentifierPath = getSiblingTreeNodeIdentifierPathWithinDomElement($(e.item), 'next'); + + getPublisherSubscriber().publish('view/tree/dnd/update', [$(e.item), movedFormElementIdentifierPath, previousFormElementIdentifierPath, nextFormElementIdentifierPath]); + getPublisherSubscriber().publish('view/tree/dnd/stop', [getTreeNodeIdentifierPathWithinDomElement($(e.item))]); + }, + }; + + const sortableRoot: HTMLElement = treeDomElement.get(0).querySelector('ol.' + getHelper().getDomElementClassName('sortable')); + new Sortable(sortableRoot, { + ...defaultConfiguration, + ...{ + group: 'tree-step-nodes', + put: ['tree-step-nodes'], + } + }); + + sortableRoot.querySelectorAll('ol').forEach(function (sortableList) { + new Sortable(sortableList, { + ...defaultConfiguration, + ...{ + group: 'tree-leaves-nodes', + pull: ['tree-leaves-nodes'], + } + }); + }); +} + +function saveExpanderStates(): void { + const addStates = function(formElement: FormElement) { + if (getFormElementDefinition(formElement, '_isCompositeFormElement')) { + const treeNode = getTreeNode(formElement); + if (treeNode.length) { + if (treeNode.closest('li').hasClass(getHelper().getDomElementClassName('expanded'))) { + expanderStates[formElement.get('__identifierPath')] = true; + } else { + expanderStates[formElement.get('__identifierPath')] = false; + } + } + + if (getUtility().isUndefinedOrNull(expanderStates[formElement.get('__identifierPath')])) { + expanderStates[formElement.get('__identifierPath')] = true; + } + } + + const childFormElements = formElement.get('renderables'); + if ('array' === $.type(childFormElements)) { + for (let i = 0, len = childFormElements.length; i < len; ++i) { + addStates(childFormElements[i]); + } + } + }; + addStates(getRootFormElement()); + + for (const identifierPath of Object.keys(expanderStates)) { + try { + getFormEditorApp().getFormElementByIdentifierPath(identifierPath); + } catch (error) { + delete expanderStates[identifierPath]; + } + } +} + +function loadExpanderStates(): void { + for (const identifierPath of Object.keys(expanderStates)) { + const treeNode = getTreeNode(identifierPath); + if (treeNode.length) { + if (expanderStates[identifierPath]) { + treeNode.closest('li') + .removeClass(getHelper().getDomElementClassName('collapsed')) + .addClass(getHelper().getDomElementClassName('expanded')); + } else { + treeNode.closest('li') + .addClass(getHelper().getDomElementClassName('collapsed')) + .removeClass(getHelper().getDomElementClassName('expanded')); + } + } + } +} + +/** + * @throws 1478721208 + */ +export function renderCompositeFormElementChildsAsSortableList(formElement: FormElement): JQuery { + assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478721208); + + const elementList = $('
        ').addClass(getHelper().getDomElementClassName('sortable')); + if ('array' === $.type(formElement.get('renderables'))) { + for (let i = 0, len = formElement.get('renderables').length; i < len; ++i) { + elementList.append(renderNestedSortableListItem(formElement.get('renderables')[i])); + } + } + return elementList; +} + +/** + * @publish view/tree/node/clicked + */ +export function renew(formElement?: FormElement): void { + if (getFormEditorApp().getUtility().isUndefinedOrNull(formElement)) { + formElement = getRootFormElement(); + } + saveExpanderStates(); + treeDomElement.off().empty().append(renderCompositeFormElementChildsAsSortableList(formElement)); + + // We make use of the same strategy for db click detection as the current core pagetree implementation. + // @see https://github.com/typo3/typo3/blob/260226e93c651356545e91a7c55ee63e186766d5/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js#L350 + let clicks = 0; + treeDomElement.on('click', function(e) { + const formElementIdentifierPath = $(e.target) + .closest(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')) + .attr(getHelper().getDomElementDataAttribute('elementIdentifier')); + if (getUtility().isUndefinedOrNull(formElementIdentifierPath) || !getUtility().isNonEmptyString(formElementIdentifierPath)) { + return; + } + + clicks++; + + if (clicks === 1) { + setTimeout(function() { + if (clicks === 1) { + getPublisherSubscriber().publish('view/tree/node/clicked', [formElementIdentifierPath]); + } else { + editTreeNodeLabel(formElementIdentifierPath); + } + clicks = 0; + }, 300); + } + }); + + $(getHelper().getDomElementDataIdentifierSelector('expander'), treeDomElement).on('click', function(this: HTMLElement) { + $(this).closest('li').toggleClass(getHelper().getDomElementClassName('collapsed')).toggleClass(getHelper().getDomElementClassName('expanded')); + }); + + if (configuration.isSortable) { + addSortableEvents(); + } + loadExpanderStates(); +} + +export function getAllTreeNodes(): JQuery { + return $(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'), treeDomElement); +} + +export function getTreeNodeWithinDomElement(element: HTMLElement | JQuery): JQuery { + return $(element).find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')).first(); +} + +export function getTreeNodeIdentifierPathWithinDomElement(element: HTMLElement | JQuery): string { + return getTreeNodeWithinDomElement($(element)).attr(getHelper().getDomElementDataAttribute('elementIdentifier')); +} + +export function getParentTreeNodeWithinDomElement(element: HTMLElement | JQuery): JQuery { + return $(element).parent().closest('li').find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')).first(); +} + +export function getParentTreeNodeIdentifierPathWithinDomElement( + element: HTMLElement | JQuery +): string { + return getParentTreeNodeWithinDomElement(element).attr(getHelper().getDomElementDataAttribute('elementIdentifier')); +} + +export function getSiblingTreeNodeIdentifierPathWithinDomElement( + element: HTMLElement | JQuery, + position: string +): string { + if (getUtility().isUndefinedOrNull(position)) { + position = 'prev'; + } + const formElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement(element); + element = (position === 'prev') ? $(element).prev('li') : $(element).next('li'); + return element.find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')) + .not(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKeyValue', [formElementIdentifierPath])) + .first() + .attr(getHelper().getDomElementDataAttribute('elementIdentifier')); +} + +export function setTreeNodeTitle(title?: string, formElement?: FormElement): void { + let titleContent: HTMLElement; + if (getUtility().isUndefinedOrNull(title)) { + titleContent = buildTitleByFormElement(formElement); + } else { + titleContent = document.createElement('span'); + titleContent.textContent = title; + } + + $(getHelper().getDomElementDataIdentifierSelector('title'), getTreeNode(formElement)).get(0).replaceChildren(titleContent); +} + +export function getTreeNode(formElement?: FormElement | string): JQuery { + let formElementIdentifierPath: string; + + if (typeof formElement === 'string') { + formElementIdentifierPath = formElement; + } else { + if (getUtility().isUndefinedOrNull(formElement)) { + formElementIdentifierPath = getCurrentlySelectedFormElement().get('__identifierPath'); + } else { + formElementIdentifierPath = formElement.get('__identifierPath'); + } + } + return $(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKeyValue', [formElementIdentifierPath]), treeDomElement); +} + +/** + * @throws 1478719287 + */ +export function buildTitleByFormElement(formElement: FormElement): HTMLElement { + if (getUtility().isUndefinedOrNull(formElement)) { + formElement = getCurrentlySelectedFormElement(); + } + assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478719287); + + const span = document.createElement('span'); + span.textContent = formElement.get('label') ? formElement.get('label') : formElement.get('identifier'); + const small = document.createElement('small'); + small.textContent = '(' + getFormElementDefinition(formElement, 'label') + ')'; + span.appendChild(small); + return span; +} + +export function getTreeDomElement(): JQuery { + return treeDomElement; +} + +function editTreeNodeLabel(formElementIdentifierPath: string): void { + const treeNode = getTreeNode(formElementIdentifierPath); + const titleNode = $(getHelper().getDomElementDataIdentifierSelector('title'), treeNode); + const currentTitle = titleNode.children()[0].childNodes[0].nodeValue.trim(); + const treeRootWidth = getTreeDomElement().width(); + let nodeIsEdit = true; + + const input = $('') + .attr('class', 'node-edit') + .css('top', function() { + const top = titleNode.position().top; + return top + 'px'; + }) + .css('left', titleNode.position().left + 'px') + .css('width', treeRootWidth - titleNode.position().left + 'px') + .attr('type', 'text') + .attr('value', currentTitle) + .on('click', (e: Event) => { + e.stopPropagation(); + }) + .on('keyup', function(this: HTMLInputElement, e) { + if (e.keyCode === 13 || e.keyCode === 9) { //enter || tab + const newTitle = this.value.trim(); + + if (getUtility().isNonEmptyString(newTitle) && (newTitle !== currentTitle)) { + nodeIsEdit = false; + input.remove(); + getPublisherSubscriber().publish('view/tree/node/changed', [formElementIdentifierPath, newTitle]); + } else { + nodeIsEdit = false; + input.remove(); + } + } else if (e.keyCode === 27) { //esc + nodeIsEdit = false; + input.remove(); + } + }) + .on('blur', function(this: HTMLInputElement) { + if(nodeIsEdit) { + const newTitle = this.value.trim(); + input.remove(); + if(getUtility().isNonEmptyString(newTitle) && newTitle !== currentTitle) { + getPublisherSubscriber().publish('view/tree/node/changed', [formElementIdentifierPath, newTitle]); + } + } + }); + + treeNode.append(input); + input.focus(); +} + +/** + * @throws 1478714814 + */ +export function bootstrap( + this: typeof import('./tree-component'), + _formEditorApp: FormEditor, + appendToDomElement: JQuery, + customConfiguration?: typeof defaultConfiguration +): typeof import('./tree-component') { + formEditorApp = _formEditorApp; + assert('object' === $.type(appendToDomElement), 'Invalid parameter "appendToDomElement"', 1478714814); + treeDomElement = $(appendToDomElement); + configuration = $.extend(true, defaultConfiguration, customConfiguration || {}); + Helper.bootstrap(formEditorApp); + return this; +} + +declare global { + interface PublisherSubscriberTopicArgumentsMap { + 'view/tree/node/changed': readonly [ + formElementIdentifierPath: string, + newLabel: string, + ]; + 'view/tree/node/clicked': readonly [ + formElementIdentifierPath: string + ]; + 'view/tree/render/listItemAdded': readonly [ + listItem: JQuery, + formElement: FormElement + ]; + 'view/tree/dnd/update': readonly [ + dndItem: JQuery, + movedFormElementIdentifierPath: string, + previousFormElementIdentifierPath: string, + nextFormElementIdentifierPath: string, + ]; + 'view/tree/dnd/change': readonly [ + dndItem: JQuery, + parentFormElementIdentifierPath: string, + enclosingCompositeFormElement: FormElement, + ]; + 'view/tree/dnd/stop': readonly [ + treeNodeIdentifierPathWithinDomElement: string, + ]; + } +} diff --git a/typo3/sysext/form/Resources/Public/JavaScript/backend/form-editor/tree-component.js b/typo3/sysext/form/Resources/Public/JavaScript/backend/form-editor/tree-component.js index 5261c548c9a0..aad22b4d0fd8 100644 --- a/typo3/sysext/form/Resources/Public/JavaScript/backend/form-editor/tree-component.js +++ b/typo3/sysext/form/Resources/Public/JavaScript/backend/form-editor/tree-component.js @@ -10,751 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ - -/** - * Module: @typo3/form/backend/form-editor/tree-component - */ -import $ from 'jquery'; -import * as Helper from '@typo3/form/backend/form-editor/helper.js'; -import Icons from '@typo3/backend/icons.js'; -import Sortable from 'sortablejs'; - -const { - bootstrap, - buildTitleByFormElement, - getAllTreeNodes, - getParentTreeNodeWithinDomElement, - getParentTreeNodeIdentifierPathWithinDomElement, - getSiblingTreeNodeIdentifierPathWithinDomElement, - getTreeDomElement, - getTreeNode, - getTreeNodeWithinDomElement, - getTreeNodeIdentifierPathWithinDomElement, - renderCompositeFormElementChildsAsSortableList, - renew, - setTreeNodeTitle -} = factory($, Helper, Icons); - -export { - bootstrap, - buildTitleByFormElement, - getAllTreeNodes, - getParentTreeNodeWithinDomElement, - getParentTreeNodeIdentifierPathWithinDomElement, - getSiblingTreeNodeIdentifierPathWithinDomElement, - getTreeDomElement, - getTreeNode, - getTreeNodeWithinDomElement, - getTreeNodeIdentifierPathWithinDomElement, - renderCompositeFormElementChildsAsSortableList, - renew, - setTreeNodeTitle -}; - -function factory($, Helper, Icons) { - - return (function($, Helper, Icons) { - - /** - * @private - * - * @var object - */ - var _configuration = null; - - /** - * @private - * - * @var object - */ - var _expanderStates = {}; - - /** - * @private - * - * @var object - */ - var _defaultConfiguration = { - domElementClassNames: { - collapsed: 'mjs-nestedSortable-collapsed', - expanded: 'mjs-nestedSortable-expanded', - hasChildren: 't3-form-element-has-children', - sortable: 'sortable', - svgLinkWrapper: 'svg-wrapper', - noNesting: 'mjs-nestedSortable-no-nesting' - }, - domElementDataAttributeNames: { - abstractType: 'data-element-abstract-type' - }, - domElementDataAttributeValues: { - collapse: 'actions-chevron-right', - expander: 'treeExpander', - title: 'treeTitle' - }, - isSortable: true, - svgLink: { - height: 15, - paths: { - angle: 'M0 0 V20 H15', - vertical: 'M0 0 V20 H0', - hidden: 'M0 0 V0 H0' - }, - width: 20 - } - }; - - /** - * @private - * - * @var object - */ - var _formEditorApp = null; - - /** - * @private - * - * @var object - */ - var _treeDomElement = null; - - /* ************************************************************* - * Private Methods - * ************************************************************/ - - /** - * @private - * - * @return void - * @throws 1478268638 - */ - function _helperSetup() { - assert('function' === $.type(Helper.bootstrap), - 'The view model helper does not implement the method "bootstrap"', - 1478268638 - ); - Helper.bootstrap(getFormEditorApp()); - }; - - /** - * @private - * - * @return object - */ - function getFormEditorApp() { - return _formEditorApp; - }; - - /** - * @public - * - * @param object - * @return object - */ - function getHelper(configuration) { - if (getUtility().isUndefinedOrNull(configuration)) { - return Helper.setConfiguration(_configuration); - } - return Helper.setConfiguration(configuration); - }; - - /** - * @private - * - * @return object - */ - function getUtility() { - return getFormEditorApp().getUtility(); - }; - - /** - * @private - * - * @param mixed test - * @param string message - * @param int messageCode - * @return void - */ - function assert(test, message, messageCode) { - return getFormEditorApp().assert(test, message, messageCode); - }; - - /** - * @private - * - * @return object - */ - function getRootFormElement() { - return getFormEditorApp().getRootFormElement(); - }; - - /** - * @private - * - * @return object - */ - function getCurrentlySelectedFormElement() { - return getFormEditorApp().getCurrentlySelectedFormElement(); - }; - - /** - * @private - * - * @return object - */ - function getPublisherSubscriber() { - return getFormEditorApp().getPublisherSubscriber(); - }; - - /** - * @private - * - * @param object - * @param string - * @return mixed - */ - function getFormElementDefinition(formElement, formElementDefinitionKey) { - return getFormEditorApp().getFormElementDefinition(formElement, formElementDefinitionKey); - }; - - /** - * @private - * - * @return object - */ - function _getLinkSvg(type) { - return $('' - + '' - + '' - + '' - + ''); - }; - - /** - * @private - * - * @param object - * @return object - * @publish view/tree/render/listItemAdded - * @throws 1478715704 - */ - function _renderNestedSortableListItem(formElement) { - var childFormElements, childList, expanderItem, isLastFormElementWithinParentFormElement, - listItem, listItemContent, searchElement; - assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478715704); - - isLastFormElementWithinParentFormElement = false; - if (formElement.get('__identifierPath') === getFormEditorApp().getLastFormElementWithinParentFormElement(formElement).get('__identifierPath')) { - isLastFormElementWithinParentFormElement = true; - } - - listItem = $('
      1. '); - if (!getFormElementDefinition(formElement, '_isCompositeFormElement')) { - listItem.addClass(getHelper().getDomElementClassName('noNesting')); - } - - listItemContent = $('
        ') - .attr(getHelper().getDomElementDataAttribute('elementIdentifier'), formElement.get('__identifierPath')) - .append( - $('') - .attr(getHelper().getDomElementDataAttribute('identifier'), getHelper().getDomElementDataAttributeValue('title')) - .html(buildTitleByFormElement(formElement)) - ); - - if (getFormElementDefinition(formElement, '_isCompositeFormElement')) { - listItemContent.attr(getHelper().getDomElementDataAttribute('abstractType'), 'isCompositeFormElement'); - } - if (getFormElementDefinition(formElement, '_isTopLevelFormElement')) { - listItemContent.attr(getHelper().getDomElementDataAttribute('abstractType'), 'isTopLevelFormElement'); - } - - expanderItem = $('').attr('data-identifier', getHelper().getDomElementDataAttributeValue('expander')); - listItemContent.prepend(expanderItem); - - Icons.getIcon(getFormElementDefinition(formElement, 'iconIdentifier'), Icons.sizes.small, null, Icons.states.default).then(function(icon) { - expanderItem.after( - $(icon).addClass(getHelper().getDomElementClassName('icon')) - .attr('title', 'id = ' + formElement.get('identifier')) - ); - - if (getFormElementDefinition(formElement, '_isCompositeFormElement')) { - if (formElement.get('renderables') && formElement.get('renderables').length > 0) { - Icons.getIcon(getHelper().getDomElementDataAttributeValue('collapse'), Icons.sizes.small).then(function(icon) { - expanderItem.before(_getLinkSvg('angle')).html($(icon)); - listItem.addClass(getHelper().getDomElementClassName('hasChildren')); - }); - } else { - expanderItem.before(_getLinkSvg('angle')).remove(); - } - } else { - listItemContent.prepend(_getLinkSvg('angle')); - expanderItem.remove(); - } - - searchElement = formElement.get('__parentRenderable'); - while (searchElement) { - if (searchElement.get('__identifierPath') === getRootFormElement().get('__identifierPath')) { - break; - } - - if (searchElement.get('__identifierPath') === getFormEditorApp().getLastFormElementWithinParentFormElement(searchElement).get('__identifierPath')) { - listItemContent.prepend(_getLinkSvg('hidden')); - } else { - listItemContent.prepend(_getLinkSvg('vertical')); - } - searchElement = searchElement.get('__parentRenderable'); - } - }); - listItem.append(listItemContent); - - getPublisherSubscriber().publish('view/tree/render/listItemAdded', [listItem, formElement]); - childFormElements = formElement.get('renderables'); - childList = null; - if ('array' === $.type(childFormElements)) { - childList = $('
          '); - for (var i = 0, len = childFormElements.length; i < len; ++i) { - childList.append(_renderNestedSortableListItem(childFormElements[i])); - } - } - - if (childList) { - listItem.append(childList); - } - return listItem; - }; - - /** - * @private - * - * @return void - * @publish view/tree/dnd/stop - * @publish view/tree/dnd/change - * @publish view/tree/dnd/update - */ - function _addSortableEvents() { - const defaultConfiguration = { - handle: 'div' + getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'), - draggable: 'li', - animation: 200, - fallbackTolerance: 200, - fallbackOnBody: true, - swapThreshold: 0.6, - dragClass: 'form-sortable-drag', - ghostClass: 'form-sortable-ghost', - onChange: function (e) { - let enclosingCompositeFormElement; - const parentFormElementIdentifierPath = getParentTreeNodeIdentifierPathWithinDomElement($(e.item)); - - if (parentFormElementIdentifierPath) { - enclosingCompositeFormElement = getFormEditorApp().findEnclosingCompositeFormElementWhichIsNotOnTopLevel(parentFormElementIdentifierPath); - } - getPublisherSubscriber().publish('view/tree/dnd/change', [$(e.item), parentFormElementIdentifierPath, enclosingCompositeFormElement]); - }, - onEnd: function (e) { - const movedFormElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement($(e.item)); - const previousFormElementIdentifierPath = getSiblingTreeNodeIdentifierPathWithinDomElement($(e.item), 'prev'); - const nextFormElementIdentifierPath = getSiblingTreeNodeIdentifierPathWithinDomElement($(e.item), 'next'); - - getPublisherSubscriber().publish('view/tree/dnd/update', [$(e.item), movedFormElementIdentifierPath, previousFormElementIdentifierPath, nextFormElementIdentifierPath]); - getPublisherSubscriber().publish('view/tree/dnd/stop', [getTreeNodeIdentifierPathWithinDomElement($(e.item))]); - }, - }; - - const sortableRoot = _treeDomElement.get(0).querySelector('ol.' + getHelper().getDomElementClassName('sortable')); - new Sortable(sortableRoot, { - ...defaultConfiguration, - ...{ - group: 'tree-step-nodes', - put: ['tree-step-nodes'], - } - }); - - sortableRoot.querySelectorAll('ol').forEach(function (sortableList) { - new Sortable(sortableList, { - ...defaultConfiguration, - ...{ - group: 'tree-leaves-nodes', - pull: ['tree-leaves-nodes'], - } - }); - }); - } - - /** - * @private - * - * @return void - */ - function _saveExpanderStates() { - var addStates; - - addStates = function(formElement) { - var childFormElements, treeNode; - - if (getFormElementDefinition(formElement, '_isCompositeFormElement')) { - treeNode = getTreeNode(formElement); - if (treeNode.length) { - if (treeNode.closest('li').hasClass(getHelper().getDomElementClassName('expanded'))) { - _expanderStates[formElement.get('__identifierPath')] = true; - } else { - _expanderStates[formElement.get('__identifierPath')] = false; - } - } - - if (getUtility().isUndefinedOrNull(_expanderStates[formElement.get('__identifierPath')])) { - _expanderStates[formElement.get('__identifierPath')] = true; - } - } - - childFormElements = formElement.get('renderables'); - if ('array' === $.type(childFormElements)) { - for (var i = 0, len = childFormElements.length; i < len; ++i) { - addStates(childFormElements[i]); - } - } - }; - addStates(getRootFormElement()); - - for (var identifierPath in _expanderStates) { - if (!_expanderStates.hasOwnProperty(identifierPath)) { - continue; - } - try { - getFormEditorApp().getFormElementByIdentifierPath(identifierPath); - } catch (error) { - delete _expanderStates[identifierPath]; - } - } - }; - - /** - * @private - * - * @return void - */ - function _loadExpanderStates() { - for (var identifierPath in _expanderStates) { - var treeNode; - - if (!_expanderStates.hasOwnProperty(identifierPath)) { - continue; - } - treeNode = getTreeNode(identifierPath); - if (treeNode.length) { - if (_expanderStates[identifierPath]) { - treeNode.closest('li') - .removeClass(getHelper().getDomElementClassName('collapsed')) - .addClass(getHelper().getDomElementClassName('expanded')); - } else { - treeNode.closest('li') - .addClass(getHelper().getDomElementClassName('collapsed')) - .removeClass(getHelper().getDomElementClassName('expanded')); - } - } - } - }; - - /* ************************************************************* - * Public Methods - * ************************************************************/ - - /** - * @public - * - * @param object - * @return object - * @throws 1478721208 - */ - function renderCompositeFormElementChildsAsSortableList(formElement) { - var elementList; - assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478721208); - - elementList = $('
            ').addClass(getHelper().getDomElementClassName('sortable')); - if ('array' === $.type(formElement.get('renderables'))) { - for (var i = 0, len = formElement.get('renderables').length; i < len; ++i) { - elementList.append(_renderNestedSortableListItem(formElement.get('renderables')[i])); - } - } - return elementList; - }; - - /** - * @public - * - * @return void - * @param object - * @publish view/tree/node/clicked - */ - function renew(formElement) { - if (getFormEditorApp().getUtility().isUndefinedOrNull(formElement)) { - formElement = getRootFormElement(); - } - _saveExpanderStates(); - _treeDomElement.off().empty().append(renderCompositeFormElementChildsAsSortableList(formElement)); - - // We make use of the same strategy for db click detection as the current core pagetree implementation. - // @see https://github.com/typo3/typo3/blob/260226e93c651356545e91a7c55ee63e186766d5/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js#L350 - var clicks = 0; - _treeDomElement.on("click", function(e) { - var formElementIdentifierPath; - - formElementIdentifierPath = $(e.target) - .closest(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')) - .attr(getHelper().getDomElementDataAttribute('elementIdentifier')); - if (getUtility().isUndefinedOrNull(formElementIdentifierPath) || !getUtility().isNonEmptyString(formElementIdentifierPath)) { - return; - } - - clicks++; - - if (clicks === 1) { - setTimeout(function() { - if (clicks === 1) { - getPublisherSubscriber().publish('view/tree/node/clicked', [formElementIdentifierPath]); - } else { - _editTreeNodeLabel(formElementIdentifierPath); - } - clicks = 0; - }, 300); - } - }); - - $(getHelper().getDomElementDataIdentifierSelector('expander'), _treeDomElement).on('click', function() { - $(this).closest('li').toggleClass(getHelper().getDomElementClassName('collapsed')).toggleClass(getHelper().getDomElementClassName('expanded')); - }); - - if (_configuration['isSortable']) { - _addSortableEvents(); - } - _loadExpanderStates(); - }; - - /** - * @public - * - * @param object - * @return string - */ - function getAllTreeNodes() { - return $(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey'), _treeDomElement); - }; - - /** - * @public - * - * @param object - * @return string - */ - function getTreeNodeWithinDomElement(element) { - return $(element).find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')).first(); - }; - - /** - * @public - * - * @param object - * @return string - */ - function getTreeNodeIdentifierPathWithinDomElement(element) { - return getTreeNodeWithinDomElement($(element)).attr(getHelper().getDomElementDataAttribute('elementIdentifier')); - }; - - /** - * @public - * - * @param object - * @return string - */ - function getParentTreeNodeWithinDomElement(element) { - return $(element).parent().closest('li').find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')).first(); - }; - - /** - * @public - * - * @param object - * @return string - */ - function getParentTreeNodeIdentifierPathWithinDomElement(element) { - return getParentTreeNodeWithinDomElement(element).attr(getHelper().getDomElementDataAttribute('elementIdentifier')); - }; - - /** - * @private - * - * @param object - * @param string - * @return string - */ - function getSiblingTreeNodeIdentifierPathWithinDomElement(element, position) { - var formElementIdentifierPath; - - if (getUtility().isUndefinedOrNull(position)) { - position = 'prev'; - } - formElementIdentifierPath = getTreeNodeIdentifierPathWithinDomElement(element); - element = (position === 'prev') ? $(element).prev('li') : $(element).next('li'); - return element.find(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKey')) - .not(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKeyValue', [formElementIdentifierPath])) - .first() - .attr(getHelper().getDomElementDataAttribute('elementIdentifier')); - }; - - /** - * @public - * - * @param string - * @param object - * @return void - */ - function setTreeNodeTitle(title, formElement) { - if (getUtility().isUndefinedOrNull(title)) { - title = buildTitleByFormElement(formElement); - } - - $(getHelper().getDomElementDataIdentifierSelector('title'), getTreeNode(formElement)).html(title); - }; - - /** - * @public - * - * @param string|object - * @return object - */ - function getTreeNode(formElement) { - var formElementIdentifierPath; - - if ('string' === $.type(formElement)) { - formElementIdentifierPath = formElement; - } else { - if (getUtility().isUndefinedOrNull(formElement)) { - formElementIdentifierPath = getCurrentlySelectedFormElement().get('__identifierPath'); - } else { - formElementIdentifierPath = formElement.get('__identifierPath'); - } - } - return $(getHelper().getDomElementDataAttribute('elementIdentifier', 'bracesWithKeyValue', [formElementIdentifierPath]), _treeDomElement); - }; - - /** - * @public - * - * @param object - * @return object - * @throws 1478719287 - */ - function buildTitleByFormElement(formElement) { - if (getUtility().isUndefinedOrNull(formElement)) { - formElement = getCurrentlySelectedFormElement(); - } - assert('object' === $.type(formElement), 'Invalid parameter "formElement"', 1478719287); - - return $('') - .text((formElement.get('label') ? formElement.get('label') : formElement.get('identifier'))) - .append($('').text("(" + getFormElementDefinition(formElement, 'label') + ")")); - }; - - /** - * @public - * - * @return object - */ - function getTreeDomElement() { - return _treeDomElement; - }; - - /** - * @private - * - * @param string - */ - function _editTreeNodeLabel(formElementIdentifierPath) { - var treeNode = getTreeNode(formElementIdentifierPath); - var titleNode = $(getHelper().getDomElementDataIdentifierSelector('title'), treeNode); - var currentTitle = titleNode.children()[0].childNodes[0].nodeValue.trim(); - var treeRootWidth = getTreeDomElement().width(); - var nodeIsEdit = true; - - var input = $('') - .attr('class', 'node-edit') - .css('top', function() { - var top = titleNode.position().top; - return top + 'px'; - }) - .css('left', titleNode.position().left + 'px') - .css('width', treeRootWidth - titleNode.position().left + 'px') - .attr('type', 'text') - .attr('value', currentTitle) - .on('click', function(e) { - e.stopPropagation(); - }) - .on('keyup', function(e) { - if (e.keyCode === 13 || e.keyCode === 9) { //enter || tab - var newTitle = this.value.trim(); - - if (getUtility().isNonEmptyString(newTitle) && (newTitle !== currentTitle)) { - nodeIsEdit = false; - input.remove(); - getPublisherSubscriber().publish('view/tree/node/changed', [formElementIdentifierPath, newTitle]); - } else { - nodeIsEdit = false; - input.remove(); - } - } else if (e.keyCode === 27) { //esc - nodeIsEdit = false; - input.remove(); - } - }) - .on('blur', function() { - if(nodeIsEdit) { - var newTitle = this.value.trim(); - input.remove(); - if(getUtility().isNonEmptyString(newTitle) && newTitle !== currentTitle) { - getPublisherSubscriber().publish('view/tree/node/changed', [formElementIdentifierPath, newTitle]); - } - } - }); - - treeNode.append(input); - input.focus(); - }; - - /** - * @public - * - * @param object - * @param object - * @param object - * @return this - * @throws 1478714814 - */ - function bootstrap(formEditorApp, appendToDomElement, configuration) { - _formEditorApp = formEditorApp; - assert('object' === $.type(appendToDomElement), 'Invalid parameter "appendToDomElement"', 1478714814); - - _treeDomElement = $(appendToDomElement); - _configuration = $.extend(true, _defaultConfiguration, configuration || {}); - _helperSetup(); - return this; - }; - - /** - * Publish the public methods. - * Implements the "Revealing Module Pattern". - */ - return { - bootstrap: bootstrap, - buildTitleByFormElement: buildTitleByFormElement, - getAllTreeNodes: getAllTreeNodes, - getParentTreeNodeWithinDomElement: getParentTreeNodeWithinDomElement, - getParentTreeNodeIdentifierPathWithinDomElement: getParentTreeNodeIdentifierPathWithinDomElement, - getSiblingTreeNodeIdentifierPathWithinDomElement: getSiblingTreeNodeIdentifierPathWithinDomElement, - getTreeDomElement: getTreeDomElement, - getTreeNode: getTreeNode, - getTreeNodeWithinDomElement: getTreeNodeWithinDomElement, - getTreeNodeIdentifierPathWithinDomElement: getTreeNodeIdentifierPathWithinDomElement, - renderCompositeFormElementChildsAsSortableList: renderCompositeFormElementChildsAsSortableList, - renew: renew, - setTreeNodeTitle: setTreeNodeTitle - }; - })($, Helper, Icons); -} +import $ from"jquery";import*as Helper from"@typo3/form/backend/form-editor/helper.js";import Icons from"@typo3/backend/icons.js";import Sortable from"sortablejs";const defaultConfiguration={domElementClassNames:{collapsed:"mjs-nestedSortable-collapsed",expanded:"mjs-nestedSortable-expanded",hasChildren:"t3-form-element-has-children",sortable:"sortable",svgLinkWrapper:"svg-wrapper",noNesting:"mjs-nestedSortable-no-nesting"},domElementDataAttributeNames:{abstractType:"data-element-abstract-type"},domElementDataAttributeValues:{collapse:"actions-chevron-right",expander:"treeExpander",title:"treeTitle"},isSortable:!0,svgLink:{height:15,paths:{angle:"M0 0 V20 H15",vertical:"M0 0 V20 H0",hidden:"M0 0 V0 H0"},width:20}};let configuration=null,formEditorApp=null,treeDomElement=null;const expanderStates={};function getFormEditorApp(){return formEditorApp}function getHelper(e){return getUtility().isUndefinedOrNull(e)?Helper.setConfiguration(configuration):Helper.setConfiguration(e)}function getUtility(){return getFormEditorApp().getUtility()}function assert(e,t,n){return getFormEditorApp().assert(e,t,n)}function getRootFormElement(){return getFormEditorApp().getRootFormElement()}function getCurrentlySelectedFormElement(){return getFormEditorApp().getCurrentlySelectedFormElement()}function getPublisherSubscriber(){return getFormEditorApp().getPublisherSubscriber()}function getFormElementDefinition(e,t){return getFormEditorApp().getFormElementDefinition(e,t)}function getLinkSvg(e){return $('')}function renderNestedSortableListItem(e){assert("object"===$.type(e),'Invalid parameter "formElement"',1478715704);const t=$("
          1. ");getFormElementDefinition(e,"_isCompositeFormElement")||t.addClass(getHelper().getDomElementClassName("noNesting"));const n=$("
            ").attr(getHelper().getDomElementDataAttribute("elementIdentifier"),e.get("__identifierPath")).append($("").attr(getHelper().getDomElementDataAttribute("identifier"),getHelper().getDomElementDataAttributeValue("title")).append(buildTitleByFormElement(e)));getFormElementDefinition(e,"_isCompositeFormElement")&&n.attr(getHelper().getDomElementDataAttribute("abstractType"),"isCompositeFormElement"),getFormElementDefinition(e,"_isTopLevelFormElement")&&n.attr(getHelper().getDomElementDataAttribute("abstractType"),"isTopLevelFormElement");const r=$("").attr("data-identifier",getHelper().getDomElementDataAttributeValue("expander"));n.prepend(r),Icons.getIcon(getFormElementDefinition(e,"iconIdentifier"),Icons.sizes.small,null,Icons.states.default).then((function(i){r.after($(i).addClass(getHelper().getDomElementClassName("icon")).attr("title","id = "+e.get("identifier"))),getFormElementDefinition(e,"_isCompositeFormElement")?e.get("renderables")&&e.get("renderables").length>0?Icons.getIcon(getHelper().getDomElementDataAttributeValue("collapse"),Icons.sizes.small).then((function(e){r.before(getLinkSvg("angle")).html(e),t.addClass(getHelper().getDomElementClassName("hasChildren"))})):r.before(getLinkSvg("angle")).remove():(n.prepend(getLinkSvg("angle")),r.remove());let o=e.get("__parentRenderable");for(;o&&o.get("__identifierPath")!==getRootFormElement().get("__identifierPath");)o.get("__identifierPath")===getFormEditorApp().getLastFormElementWithinParentFormElement(o).get("__identifierPath")?n.prepend(getLinkSvg("hidden")):n.prepend(getLinkSvg("vertical")),o=o.get("__parentRenderable")})),t.append(n),getPublisherSubscriber().publish("view/tree/render/listItemAdded",[t,e]);const i=e.get("renderables");let o=null;if("array"===$.type(i)){o=$("
              ");for(let e=0,t=i.length;e").addClass(getHelper().getDomElementClassName("sortable"));if("array"===$.type(e.get("renderables")))for(let n=0,r=e.get("renderables").length;n").attr("class","node-edit").css("top",(function(){return n.position().top+"px"})).css("left",n.position().left+"px").css("width",i-n.position().left+"px").attr("type","text").attr("value",r).on("click",(e=>{e.stopPropagation()})).on("keyup",(function(t){if(13===t.keyCode||9===t.keyCode){const t=this.value.trim();getUtility().isNonEmptyString(t)&&t!==r?(o=!1,l.remove(),getPublisherSubscriber().publish("view/tree/node/changed",[e,t])):(o=!1,l.remove())}else 27===t.keyCode&&(o=!1,l.remove())})).on("blur",(function(){if(o){const t=this.value.trim();l.remove(),getUtility().isNonEmptyString(t)&&t!==r&&getPublisherSubscriber().publish("view/tree/node/changed",[e,t])}}));t.append(l),l.focus()}export function bootstrap(e,t,n){return formEditorApp=e,assert("object"===$.type(t),'Invalid parameter "appendToDomElement"',1478714814),treeDomElement=$(t),configuration=$.extend(!0,defaultConfiguration,n||{}),Helper.bootstrap(formEditorApp),this} \ No newline at end of file