diff --git a/Build/Sources/Sass/component/_tree.scss b/Build/Sources/Sass/component/_tree.scss index 0a3093a5c45374add2bf303f892582210fa47ffd..bd024a63941ea4ebdd2d65e4a62af0dac32327e0 100644 --- a/Build/Sources/Sass/component/_tree.scss +++ b/Build/Sources/Sass/component/_tree.scss @@ -168,6 +168,17 @@ } } +.node-label { + position: absolute; + border-radius: .25rem; + top: 4px; + inset-inline-start: 4px; + bottom: 4px; + width: .25rem; + user-select: none; + pointer-events: none; +} + .node-treelines { display: flex; flex-shrink: 0; @@ -270,7 +281,7 @@ overflow: hidden; } -.node-label { +.node-contentlabel { display: flex; flex-grow: 1; flex-wrap: wrap; diff --git a/Build/Sources/TypeScript/backend/tree/page-tree-element.ts b/Build/Sources/TypeScript/backend/tree/page-tree-element.ts index 3639a14832c12670ee5c910a9601b70ca585b88e..a1257d83ecbab79632b744496df9a230e672db55 100644 --- a/Build/Sources/TypeScript/backend/tree/page-tree-element.ts +++ b/Build/Sources/TypeScript/backend/tree/page-tree-element.ts @@ -574,6 +574,7 @@ class PageTreeToolbar extends TreeToolbar { type: 'PageTreeItem', doktype: item.nodeType, statusInformation: [], + labels: [], }; this.tree.draggingNode = newNode; this.tree.nodeDragMode = TreeNodeCommandEnum.NEW; diff --git a/Build/Sources/TypeScript/backend/tree/tree-node.ts b/Build/Sources/TypeScript/backend/tree/tree-node.ts index 7c3a0a1a4576b9c272e1e2fe83189efabce8230d..2a789391ddd9df9518cf29c4e338024df738d5fb 100644 --- a/Build/Sources/TypeScript/backend/tree/tree-node.ts +++ b/Build/Sources/TypeScript/backend/tree/tree-node.ts @@ -34,6 +34,12 @@ export interface TreeNodeStatusInformation { priority: number } +export interface TreeNodeLabel { + label: string, + color: string, + priority: number, +} + /** * Represents a single node in the tree that is rendered. */ @@ -58,6 +64,7 @@ export interface TreeNodeInterface { icon: string, overlayIcon: string, statusInformation: Array<TreeNodeStatusInformation>, + labels: Array<TreeNodeLabel>, // Calculated Internal __treeIdentifier: string, diff --git a/Build/Sources/TypeScript/backend/tree/tree.ts b/Build/Sources/TypeScript/backend/tree/tree.ts index 21a2b6fa8bb1c3b662476f3dcf715d61fa56a104..05ee45c52796382dee894be9b98b9666f414ad00 100644 --- a/Build/Sources/TypeScript/backend/tree/tree.ts +++ b/Build/Sources/TypeScript/backend/tree/tree.ts @@ -16,7 +16,7 @@ import { property, state, query } from 'lit/decorators'; import { repeat } from 'lit/directives/repeat'; import { styleMap } from 'lit/directives/style-map'; import { ifDefined } from 'lit/directives/if-defined'; -import { TreeNodeInterface, TreeNodeCommandEnum, TreeNodePositionEnum, TreeNodeStatusInformation } from './tree-node'; +import { TreeNodeInterface, TreeNodeCommandEnum, TreeNodePositionEnum, TreeNodeStatusInformation, TreeNodeLabel } from './tree-node'; import AjaxRequest from '@typo3/core/ajax/ajax-request'; import Notification from '../notification'; import { KeyTypesEnum as KeyTypes } from '../enum/key-types'; @@ -617,6 +617,7 @@ export class Tree extends LitElement { @focusout="${() => { if (this.focusedNode === node) { this.focusedNode = null; } }}" @contextmenu="${(event: MouseEvent) => { event.preventDefault(); event.stopPropagation(); this.dispatchEvent(new CustomEvent('typo3:tree:node-context', { detail: { node: node } })); }}" > + ${this.createNodeLabel(node)} ${this.createNodeGuides(node)} ${this.createNodeLoader(node) || this.createNodeToggle(node) || nothing} ${this.createNodeContent(node)} @@ -914,6 +915,21 @@ export class Tree extends LitElement { // // Node Rendering // + protected createNodeLabel(node: TreeNodeInterface): TemplateResult + { + const labels = this.getNodeLabels(node); + if (labels.length === 0) { + return html`${nothing}`; + } + + const label = labels[0]; + const styles = { backgroundColor: label.color }; + + return html` + <span class="node-label" style=${styleMap(styles)}></span> + `; + } + protected createNodeGuides(node: TreeNodeInterface): TemplateResult { const guides = node.__treeParents.map((treeIdentifier) => { @@ -1016,7 +1032,7 @@ export class Tree extends LitElement { } return html` - <div class="node-label"> + <div class="node-contentlabel"> <div class="node-name" .innerHTML="${label}"></div> ${node.note ? html`<div class="node-note">${node.note}</div>` : nothing } </div>`; @@ -1177,6 +1193,24 @@ export class Tree extends LitElement { return classList; } + protected getNodeLabels(node: TreeNodeInterface): TreeNodeLabel[] { + let labels = node.labels; + if (labels.length > 0) { + labels = labels.sort((a, b) => { + return b.priority - a.priority; + }); + + return labels; + } + + const parentNode = this.getParentNode(node); + if (parentNode === null) { + return []; + } + + return this.getNodeLabels(parentNode); + } + protected getNodeStatusInformation(node: TreeNodeInterface): TreeNodeStatusInformation[] { if (node.statusInformation.length === 0) { return []; @@ -1276,9 +1310,15 @@ export class Tree extends LitElement { protected getNodeTitle(node: TreeNodeInterface): string { let baseNodeTitle = node.tooltip ? node.tooltip : 'uid=' + node.identifier + ' ' + node.name; + + const labels = this.getNodeLabels(node); + if (labels.length) { + baseNodeTitle += '; ' + labels.map(label => label.label).join('; '); + } + const statusInformation = this.getNodeStatusInformation(node); if (statusInformation.length) { - baseNodeTitle += '; ' + statusInformation.map(node => node.label).join('; '); + baseNodeTitle += '; ' + statusInformation.map(information => information.label).join('; '); } return baseNodeTitle; diff --git a/typo3/sysext/backend/Classes/Controller/FileStorage/TreeController.php b/typo3/sysext/backend/Classes/Controller/FileStorage/TreeController.php index 5343b1e0ce1780865e6ae139d425f38c9fdddaa8..5c02722c685ecac95e15fac0f9eb360e276e5bb7 100644 --- a/typo3/sysext/backend/Classes/Controller/FileStorage/TreeController.php +++ b/typo3/sysext/backend/Classes/Controller/FileStorage/TreeController.php @@ -163,6 +163,7 @@ class TreeController icon: $item['icon'], overlayIcon: $item['overlayIcon'], statusInformation: (array)($item['statusInformation'] ?? []), + labels: (array)($item['labels'] ?? []), ), pathIdentifier: (string)($item['pathIdentifier'] ?? ''), storage: (int)($item['storage'] ?? 0), diff --git a/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php b/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php index f2e8826dc0f63355326f1e1cf0aae69f629727ad..fe8c8588c72422c219fe8067987eb05b111c18b1 100644 --- a/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php +++ b/typo3/sysext/backend/Classes/Controller/FormSelectTreeAjaxController.php @@ -211,6 +211,7 @@ class FormSelectTreeAjaxController icon: (string)($item['icon'] ?? ''), overlayIcon: (string)($item['overlayIcon'] ?? ''), statusInformation: (array)($item['statusInformation'] ?? []), + labels: (array)($item['labels'] ?? []), ), checked: (bool)($item['checked'] ?? false), selectable: (bool)($item['selectable'] ?? false), diff --git a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php index 65227c5e4a59c73d7ed25aad0e2ded04645db670..be5f5719b48495314a3f4201ff4055f11644d19b 100644 --- a/typo3/sysext/backend/Classes/Controller/Page/TreeController.php +++ b/typo3/sysext/backend/Classes/Controller/Page/TreeController.php @@ -21,6 +21,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent; +use TYPO3\CMS\Backend\Dto\Tree\Label\Label; use TYPO3\CMS\Backend\Dto\Tree\PageTreeItem; use TYPO3\CMS\Backend\Dto\Tree\TreeItem; use TYPO3\CMS\Backend\Routing\UriBuilder; @@ -80,6 +81,21 @@ class TreeController */ protected $hiddenRecords = []; + /** + * An array of background colors for a branch in the tree, set via userTS. + * + * @var array + * @deprecated will be removed in TYPO3 v14.0, please use labels instead + */ + protected $backgroundColors = []; + + /** + * An array of labels for a branch in the tree, set via userTS. + * + * @var array + */ + protected array $labels = []; + /** * Contains the state of all items that are expanded. * @@ -147,6 +163,8 @@ class TreeController (string)($userTsConfig['options.']['hideRecords.']['pages'] ?? ''), true ); + $this->backgroundColors = $userTsConfig['options.']['pageTree.']['backgroundColor.'] ?? []; + $this->labels = $userTsConfig['options.']['pageTree.']['label.'] ?? []; $this->addIdAsPrefix = (bool)($userTsConfig['options.']['pageTree.']['showPageIdWithTitle'] ?? false); $this->addDomainName = (bool)($userTsConfig['options.']['pageTree.']['showDomainNameWithTitle'] ?? false); $this->useNavTitle = (bool)($userTsConfig['options.']['pageTree.']['showNavTitle'] ?? false); @@ -368,6 +386,21 @@ class TreeController $prefix = '[' . $pageId . '] '; } + $labels = []; + if (!empty($this->backgroundColors[$pageId])) { + $labels[] = new Label( + label: $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.color') . ': ' . $this->backgroundColors[$pageId], + color: $this->backgroundColors[$pageId] ?? '#ff0000', + priority: -1, + ); + } + if (!empty($this->labels[$pageId . '.']) && isset($this->labels[$pageId . '.']['label']) && trim($this->labels[$pageId . '.']['label']) !== '') { + $labels[] = new Label( + label: (string)($this->labels[$pageId . '.']['label']), + color: (string)($this->labels[$pageId . '.']['color'] ?? '#ff8700'), + ); + } + $editable = false; if ($pageId !== 0) { $editable = $this->userHasAccessToModifyPagesAndToDefaultLanguage && $backendUser->doesUserHaveAccess($page, Permission::PAGE_EDIT); @@ -389,6 +422,7 @@ class TreeController 'expanded' => $expanded, 'editable' => $editable, 'deletable' => $backendUser->doesUserHaveAccess($page, Permission::PAGE_DELETE), + 'labels' => $labels, // _page is only for use in events so they do not need to fetch those // records again. The property will be removed from the final payload. @@ -635,6 +669,7 @@ class TreeController icon: (string)($item['icon'] ?? ''), overlayIcon: (string)($item['overlayIcon'] ?? ''), statusInformation: (array)($item['statusInformation'] ?? []), + labels: (array)($item['labels'] ?? []), ), // PageTreeItem doktype: (int)($item['doktype'] ?? ''), diff --git a/typo3/sysext/backend/Classes/Dto/Tree/Label/Label.php b/typo3/sysext/backend/Classes/Dto/Tree/Label/Label.php new file mode 100644 index 0000000000000000000000000000000000000000..219ece9b6981e59b05012f2929747a6f1e723ce5 --- /dev/null +++ b/typo3/sysext/backend/Classes/Dto/Tree/Label/Label.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +/* + * 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! + */ + +namespace TYPO3\CMS\Backend\Dto\Tree\Label; + +/** + * @internal + */ +final readonly class Label implements \JsonSerializable +{ + public function __construct( + public string $label, + public string $color = '#ff8700', + public int $priority = 0, + ) {} + + public function jsonSerialize(): array + { + return get_object_vars($this); + } +} diff --git a/typo3/sysext/backend/Classes/Dto/Tree/TreeItem.php b/typo3/sysext/backend/Classes/Dto/Tree/TreeItem.php index 0d376f115886ba4f5ec244fd040404d0e44111db..65a5a3694317e6eebc0cafef437ac687f0024898 100644 --- a/typo3/sysext/backend/Classes/Dto/Tree/TreeItem.php +++ b/typo3/sysext/backend/Classes/Dto/Tree/TreeItem.php @@ -17,6 +17,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Backend\Dto\Tree; +use TYPO3\CMS\Backend\Dto\Tree\Label\Label; use TYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation; /** @@ -26,6 +27,7 @@ final readonly class TreeItem implements \JsonSerializable { /** * @param StatusInformation[] $statusInformation + * @param Label[] $labels **/ public function __construct( public string $identifier, @@ -43,6 +45,7 @@ final readonly class TreeItem implements \JsonSerializable public string $overlayIcon, public string $note = '', public array $statusInformation = [], + public array $labels = [], public bool $editable = false, public bool $deletable = false, ) {} diff --git a/typo3/sysext/backend/Resources/Public/Css/backend.css b/typo3/sysext/backend/Resources/Public/Css/backend.css index 3ea3e7090ae8bde94341aca933229842d03a196c..5be4e5c3e1678a535d78e3c582b7d77f71165e03 100644 --- a/typo3/sysext/backend/Resources/Public/Css/backend.css +++ b/typo3/sysext/backend/Resources/Public/Css/backend.css @@ -3356,6 +3356,7 @@ to{opacity:1;transform:translate3d(0,0,0)} .node-dragging-after .node-content:after,.node-dragging-before .node-content:after{content:"";pointer-events:none;position:absolute;width:100%;left:0;height:2px;background-color:var(--tree-drop-position-bg)} .node-dragging-before .node-content:after{top:0} .node-dragging-after .node-content:after{bottom:0} +.node-label{position:absolute;border-radius:.25rem;top:4px;inset-inline-start:4px;bottom:4px;width:.25rem;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none} .node-treelines{display:flex;flex-shrink:0;height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none} .node-treeline{position:relative;flex-shrink:0;width:20px;height:100%;color:inherit;opacity:.15} .node-treeline:after,.node-treeline:before{content:"";position:absolute;background-color:currentColor} @@ -3369,7 +3370,7 @@ to{opacity:1;transform:translate3d(0,0,0)} .node-treelines+.node-loading,.node-treelines+.node-stop,.node-treelines+.node-toggle{margin-inline-start:-20px} .node-treelines+.node-loading typo3-backend-icon,.node-treelines+.node-stop typo3-backend-icon,.node-treelines+.node-toggle typo3-backend-icon{position:relative;background-color:var(--tree-node-bg)} .node-content{position:relative;display:flex;height:100%;flex-grow:1;overflow:hidden} -.node-label{display:flex;flex-grow:1;flex-wrap:wrap;align-items:center;overflow:hidden;padding-inline-start:.25rem} +.node-contentlabel{display:flex;flex-grow:1;flex-wrap:wrap;align-items:center;overflow:hidden;padding-inline-start:.25rem} .node-name,.node-note{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%;min-width:0;pointer-events:none} .node-note{margin-top:-.65em;font-size:10px;opacity:.65} .node-edit{display:flex;flex-grow:1;width:100%;padding:0;padding-inline-start:calc(.25rem - 1px);border:1px solid var(--tree-node-border-color);color:var(--typo3-component-color);background:var(--typo3-component-bg);outline:0} diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/tree/page-tree-element.js b/typo3/sysext/backend/Resources/Public/JavaScript/tree/page-tree-element.js index 15e9e7094ec22dc58179edb21110d74780ffd132..e428485036afb4c6dcfb07e7c817c731642b2d26 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/tree/page-tree-element.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/tree/page-tree-element.js @@ -79,4 +79,4 @@ var __decorate=function(e,t,o,n){var a,r=arguments.length,i=r<3?t:null===n?n=Obj </ul> </div> </div> - `}handleDragStart(e,t){const o={__hidden:!1,__indeterminate:!1,__loading:!1,__processed:!1,__treeDragAction:"",__treeIdentifier:"",__treeParents:[""],__parents:[""],__x:0,__y:0,deletable:!1,depth:0,editable:!0,expanded:!1,hasChildren:!1,icon:t.icon,overlayIcon:"",identifier:"NEW"+Math.floor(1e9*Math.random()).toString(16),loaded:!1,name:"",note:"",parentIdentifier:"",prefix:"",recordType:"pages",suffix:"",tooltip:"",type:"PageTreeItem",doktype:t.nodeType,statusInformation:[]};this.tree.draggingNode=o,this.tree.nodeDragMode=TreeNodeCommandEnum.NEW,e.dataTransfer.clearData();const n={statusIconIdentifier:this.tree.getNodeDragStatusIcon(),tooltipIconIdentifier:t.icon,tooltipLabel:t.title};e.dataTransfer.setData(DataTransferTypes.dragTooltip,JSON.stringify(n)),e.dataTransfer.setData(DataTransferTypes.newTreenode,JSON.stringify(o)),e.dataTransfer.effectAllowed="move"}};__decorate([property({type:EditablePageTree})],PageTreeToolbar.prototype,"tree",void 0),PageTreeToolbar=__decorate([customElement("typo3-backend-navigation-component-pagetree-toolbar")],PageTreeToolbar); \ No newline at end of file + `}handleDragStart(e,t){const o={__hidden:!1,__indeterminate:!1,__loading:!1,__processed:!1,__treeDragAction:"",__treeIdentifier:"",__treeParents:[""],__parents:[""],__x:0,__y:0,deletable:!1,depth:0,editable:!0,expanded:!1,hasChildren:!1,icon:t.icon,overlayIcon:"",identifier:"NEW"+Math.floor(1e9*Math.random()).toString(16),loaded:!1,name:"",note:"",parentIdentifier:"",prefix:"",recordType:"pages",suffix:"",tooltip:"",type:"PageTreeItem",doktype:t.nodeType,statusInformation:[],labels:[]};this.tree.draggingNode=o,this.tree.nodeDragMode=TreeNodeCommandEnum.NEW,e.dataTransfer.clearData();const n={statusIconIdentifier:this.tree.getNodeDragStatusIcon(),tooltipIconIdentifier:t.icon,tooltipLabel:t.title};e.dataTransfer.setData(DataTransferTypes.dragTooltip,JSON.stringify(n)),e.dataTransfer.setData(DataTransferTypes.newTreenode,JSON.stringify(o)),e.dataTransfer.effectAllowed="move"}};__decorate([property({type:EditablePageTree})],PageTreeToolbar.prototype,"tree",void 0),PageTreeToolbar=__decorate([customElement("typo3-backend-navigation-component-pagetree-toolbar")],PageTreeToolbar); \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js b/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js index 536322e82f53d2d03d7f2402389acc4597a0374a..2b6cc293060f8e2763fe436e3fa0f604a51bbd63 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/tree/tree.js @@ -58,6 +58,7 @@ var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Obj @focusout="${()=>{this.focusedNode===e&&(this.focusedNode=null)}}" @contextmenu="${t=>{t.preventDefault(),t.stopPropagation(),this.dispatchEvent(new CustomEvent("typo3:tree:node-context",{detail:{node:e}}))}}" > + ${this.createNodeLabel(e)} ${this.createNodeGuides(e)} ${this.createNodeLoader(e)||this.createNodeToggle(e)||nothing} ${this.createNodeContent(e)} @@ -66,7 +67,9 @@ var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Obj </div> `))} </div> - `}firstUpdated(){new ResizeObserver((e=>{for(const t of e)t.target===this.root&&(this.currentVisibleHeight=t.target.getBoundingClientRect().height)})).observe(this.root),Object.assign(this.settings,this.setup||{}),this.registerUnloadHandler(),this.loadData(),this.dispatchEvent(new Event("tree:initialized"))}resetSelectedNodes(){this.getSelectedNodes().forEach((e=>{!0===e.checked&&(e.checked=!1)}))}isNodeSelectable(e){return!0}isNodeEditable(e){return e.editable&&this.allowNodeEdit}handleNodeClick(e,t){1===e.detail&&(e.preventDefault(),e.stopPropagation(),this.editingNode!==t&&this.selectNode(t,!0))}handleNodeDoubleClick(e,t){e.preventDefault(),e.stopPropagation(),this.editingNode!==t&&this.editNode(t)}cleanDrag(){this.querySelectorAll(".node").forEach((function(e){e.classList.remove("node-dragging-before"),e.classList.remove("node-dragging-after"),e.classList.remove("node-hover")}))}getNodeFromDragEvent(e){const t=e.target;return this.getNodeFromElement(t.closest("[data-tree-id]"))}getTooltipDescription(e){return"ID: "+e.identifier}handleNodeDragStart(e,t){if(!1===this.allowNodeDrag||0===t.depth)return void e.preventDefault();this.draggingNode=t,this.requestUpdate(),e.dataTransfer.clearData();const i={statusIconIdentifier:this.getNodeDragStatusIcon(),tooltipIconIdentifier:t.icon,tooltipLabel:t.name,tooltipDescription:this.getTooltipDescription(t)};e.dataTransfer.setData(DataTransferTypes.dragTooltip,JSON.stringify(i)),this.createDataTransferItemsFromNode(t).forEach((({data:t,type:i})=>e.dataTransfer.items.add(t,i))),e.dataTransfer.effectAllowed="move"}handleNodeDragOver(e){if(!e.dataTransfer.types.includes(DataTransferTypes.treenode)&&!e.dataTransfer.types.includes(DataTransferTypes.newTreenode))return!1;const t=e.target,i=this.getNodeFromDragEvent(e);if(null===i)return!1;if(null===this.draggingNode)return!1;this.cleanDrag(),this.refreshDragToolTip(),this.nodeDragMode=null,this.nodeDragPosition=null;const o=this.getElementFromNode(i);if(o.classList.add("node-hover"),i.hasChildren&&!i.expanded?this.openNodeTimeout.targetNode!=i&&(this.openNodeTimeout.targetNode=i,clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.timeout=setTimeout((()=>{this.showChildren(this.openNodeTimeout.targetNode),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null}),1e3)):(clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null),this.draggingNode==i){return"delete"===t.dataset.treeDropzone?(this.nodeDragMode=TreeNodeCommandEnum.DELETE,e.preventDefault(),this.refreshDragToolTip(),!0):(this.refreshDragToolTip(),!0)}if(i.__parents.includes(this.draggingNode.identifier))return this.refreshDragToolTip(),!0;if(this.nodeDragMode=TreeNodeCommandEnum.MOVE,e.dataTransfer.types.includes(DataTransferTypes.newTreenode)&&(this.nodeDragMode=TreeNodeCommandEnum.NEW),this.nodeDragPosition=TreeNodePositionEnum.INSIDE,0===i.depth||!1===this.allowNodeSorting)return this.refreshDragToolTip(),e.preventDefault(),!0;const n=this.getElementFromNode(i).getBoundingClientRect(),s=e.clientY-n.y;return s<6?(this.nodeDragPosition=TreeNodePositionEnum.BEFORE,o.classList.add("node-dragging-before")):this.nodeHeight-s<6&&!1===i.hasChildren&&!1===i.expanded&&(this.nodeDragPosition=TreeNodePositionEnum.AFTER,o.classList.add("node-dragging-after")),this.refreshDragToolTip(),e.preventDefault(),!0}handleNodeDragLeave(e){null!==this.draggingNode&&this.cleanDrag()}handleNodeDragEnd(e){this.cleanDrag(),this.draggingNode=null,this.requestUpdate()}handleNodeDrop(e){if(this.cleanDrag(),e.dataTransfer.types.includes(DataTransferTypes.treenode)){e.preventDefault();const t=e.dataTransfer.getData(DataTransferTypes.treenode),i=this.getNodeByTreeIdentifier(t);this.nodeDragMode===TreeNodeCommandEnum.DELETE&&this.deleteNode(i);const o=this.getNodeFromDragEvent(e);return null!==o&&(this.nodeDragMode===TreeNodeCommandEnum.MOVE&&this.moveNode(i,o,this.nodeDragPosition),this.nodeDragMode=null,this.nodeDragPosition=null,!0)}if(e.dataTransfer.types.includes(DataTransferTypes.newTreenode)){e.preventDefault();const t=this.getNodeFromDragEvent(e);if(null===t)return!1;const i=e.dataTransfer.getData(DataTransferTypes.newTreenode);return this.addNode(JSON.parse(i),t,this.nodeDragPosition),this.nodeDragMode=null,this.nodeDragPosition=null,!0}return!1}refreshDragToolTip(){top.document.dispatchEvent(new CustomEvent("typo3:drag-tooltip:metadata-update",{detail:{statusIconIdentifier:this.getNodeDragStatusIcon()}}))}createNodeGuides(e){const t=e.__treeParents.map((e=>{const t=this.getNodeByTreeIdentifier(e);let i="none";return this.getNodeSetsize(t)!==this.getNodePositionInSet(t)&&(i="line"),html` + `}firstUpdated(){new ResizeObserver((e=>{for(const t of e)t.target===this.root&&(this.currentVisibleHeight=t.target.getBoundingClientRect().height)})).observe(this.root),Object.assign(this.settings,this.setup||{}),this.registerUnloadHandler(),this.loadData(),this.dispatchEvent(new Event("tree:initialized"))}resetSelectedNodes(){this.getSelectedNodes().forEach((e=>{!0===e.checked&&(e.checked=!1)}))}isNodeSelectable(e){return!0}isNodeEditable(e){return e.editable&&this.allowNodeEdit}handleNodeClick(e,t){1===e.detail&&(e.preventDefault(),e.stopPropagation(),this.editingNode!==t&&this.selectNode(t,!0))}handleNodeDoubleClick(e,t){e.preventDefault(),e.stopPropagation(),this.editingNode!==t&&this.editNode(t)}cleanDrag(){this.querySelectorAll(".node").forEach((function(e){e.classList.remove("node-dragging-before"),e.classList.remove("node-dragging-after"),e.classList.remove("node-hover")}))}getNodeFromDragEvent(e){const t=e.target;return this.getNodeFromElement(t.closest("[data-tree-id]"))}getTooltipDescription(e){return"ID: "+e.identifier}handleNodeDragStart(e,t){if(!1===this.allowNodeDrag||0===t.depth)return void e.preventDefault();this.draggingNode=t,this.requestUpdate(),e.dataTransfer.clearData();const i={statusIconIdentifier:this.getNodeDragStatusIcon(),tooltipIconIdentifier:t.icon,tooltipLabel:t.name,tooltipDescription:this.getTooltipDescription(t)};e.dataTransfer.setData(DataTransferTypes.dragTooltip,JSON.stringify(i)),this.createDataTransferItemsFromNode(t).forEach((({data:t,type:i})=>e.dataTransfer.items.add(t,i))),e.dataTransfer.effectAllowed="move"}handleNodeDragOver(e){if(!e.dataTransfer.types.includes(DataTransferTypes.treenode)&&!e.dataTransfer.types.includes(DataTransferTypes.newTreenode))return!1;const t=e.target,i=this.getNodeFromDragEvent(e);if(null===i)return!1;if(null===this.draggingNode)return!1;this.cleanDrag(),this.refreshDragToolTip(),this.nodeDragMode=null,this.nodeDragPosition=null;const o=this.getElementFromNode(i);if(o.classList.add("node-hover"),i.hasChildren&&!i.expanded?this.openNodeTimeout.targetNode!=i&&(this.openNodeTimeout.targetNode=i,clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.timeout=setTimeout((()=>{this.showChildren(this.openNodeTimeout.targetNode),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null}),1e3)):(clearTimeout(this.openNodeTimeout.timeout),this.openNodeTimeout.targetNode=null,this.openNodeTimeout.timeout=null),this.draggingNode==i){return"delete"===t.dataset.treeDropzone?(this.nodeDragMode=TreeNodeCommandEnum.DELETE,e.preventDefault(),this.refreshDragToolTip(),!0):(this.refreshDragToolTip(),!0)}if(i.__parents.includes(this.draggingNode.identifier))return this.refreshDragToolTip(),!0;if(this.nodeDragMode=TreeNodeCommandEnum.MOVE,e.dataTransfer.types.includes(DataTransferTypes.newTreenode)&&(this.nodeDragMode=TreeNodeCommandEnum.NEW),this.nodeDragPosition=TreeNodePositionEnum.INSIDE,0===i.depth||!1===this.allowNodeSorting)return this.refreshDragToolTip(),e.preventDefault(),!0;const n=this.getElementFromNode(i).getBoundingClientRect(),s=e.clientY-n.y;return s<6?(this.nodeDragPosition=TreeNodePositionEnum.BEFORE,o.classList.add("node-dragging-before")):this.nodeHeight-s<6&&!1===i.hasChildren&&!1===i.expanded&&(this.nodeDragPosition=TreeNodePositionEnum.AFTER,o.classList.add("node-dragging-after")),this.refreshDragToolTip(),e.preventDefault(),!0}handleNodeDragLeave(e){null!==this.draggingNode&&this.cleanDrag()}handleNodeDragEnd(e){this.cleanDrag(),this.draggingNode=null,this.requestUpdate()}handleNodeDrop(e){if(this.cleanDrag(),e.dataTransfer.types.includes(DataTransferTypes.treenode)){e.preventDefault();const t=e.dataTransfer.getData(DataTransferTypes.treenode),i=this.getNodeByTreeIdentifier(t);this.nodeDragMode===TreeNodeCommandEnum.DELETE&&this.deleteNode(i);const o=this.getNodeFromDragEvent(e);return null!==o&&(this.nodeDragMode===TreeNodeCommandEnum.MOVE&&this.moveNode(i,o,this.nodeDragPosition),this.nodeDragMode=null,this.nodeDragPosition=null,!0)}if(e.dataTransfer.types.includes(DataTransferTypes.newTreenode)){e.preventDefault();const t=this.getNodeFromDragEvent(e);if(null===t)return!1;const i=e.dataTransfer.getData(DataTransferTypes.newTreenode);return this.addNode(JSON.parse(i),t,this.nodeDragPosition),this.nodeDragMode=null,this.nodeDragPosition=null,!0}return!1}refreshDragToolTip(){top.document.dispatchEvent(new CustomEvent("typo3:drag-tooltip:metadata-update",{detail:{statusIconIdentifier:this.getNodeDragStatusIcon()}}))}createNodeLabel(e){const t=this.getNodeLabels(e);if(0===t.length)return html`${nothing}`;const i={backgroundColor:t[0].color};return html` + <span class="node-label" style=${styleMap(i)}></span> + `}createNodeGuides(e){const t=e.__treeParents.map((e=>{const t=this.getNodeByTreeIdentifier(e);let i="none";return this.getNodeSetsize(t)!==this.getNodePositionInSet(t)&&(i="line"),html` <div class="node-treeline node-treeline--${i}" data-origin="${this.getNodeTreeIdentifier(t)}" @@ -106,7 +109,7 @@ var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Obj ></typo3-backend-icon> </span> `:html`${nothing}`}createNodeContentLabel(e){let t=(e.prefix||"")+e.name+(e.suffix||"");const i=document.createElement("div");if(i.textContent=t,t=i.innerHTML,this.searchTerm){const e=new RegExp(this.searchTerm,"gi");t=t.replace(e,'<span class="node-highlight-text">$&</span>')}return html` - <div class="node-label"> + <div class="node-contentlabel"> <div class="node-name" .innerHTML="${t}"></div> ${e.note?html`<div class="node-note">${e.note}</div>`:nothing} </div>`}createNodeStatusInformation(e){const t=this.getNodeStatusInformation(e);if(0===t.length)return html`${nothing}`;const i=t[0],o=Severity.getCssClass(i.severity),n=""!==i.icon?i.icon:"actions-dot",s=""!==i.overlayIcon?i.overlayIcon:void 0;return html` @@ -131,4 +134,4 @@ var __decorate=function(e,t,i,o){var n,s=arguments.length,r=s<3?t:null===o?o=Obj @keydown="${i=>{const o=i.key;if([KeyTypes.ENTER,KeyTypes.TAB].includes(o)){const o=i.target.value.trim();this.editingNode=null,this.requestUpdate(),o!==e.name&&""!==o?(this.handleNodeEdit(e,o),this.focusNode(e)):t===TreeNodeCommandEnum.NEW&&""===o?this.removeNode(e):this.focusNode(e)}else[KeyTypes.ESCAPE].includes(o)&&(this.editingNode=null,this.requestUpdate(),t===TreeNodeCommandEnum.NEW?this.removeNode(e):this.focusNode(e))}}" value="${e.name}" /> - `}async handleNodeEdit(e,t){console.error("The function Tree->handleNodeEdit is not implemented.")}handleNodeDelete(e){console.error("The function Tree->handleNodeDelete is not implemented.")}handleNodeMove(e,t,i){console.error("The function Tree->handleNodeMove is not implemented.")}async handleNodeAdd(e,t,i){console.error("The function Tree->handleNodeAdd is not implemented.")}createNodeContentAction(e){return html`${nothing}`}createDataTransferItemsFromNode(e){throw new Error("The function Tree->createDataTransferItemFromNode is not implemented.")}getNodeIdentifier(e){return e.identifier}getNodeTreeIdentifier(e){return e.__treeIdentifier}getNodeParentTreeIdentifier(e){return e.__parents.join("_")}getNodeClasses(e){const t=["node"];return e.checked&&t.push("node-selected"),this.draggingNode===e&&t.push("node-dragging"),t}getNodeStatusInformation(e){if(0===e.statusInformation.length)return[];return e.statusInformation.sort(((e,t)=>e.severity!==t.severity?t.severity-e.severity:t.priority-e.priority))}getNodeDepth(e){return e.depth}getNodeChildren(e){return e.hasChildren?this.displayNodes.filter((t=>e===this.getParentNode(t))):[]}getNodeSetsize(e){if(0===e.depth)return this.displayNodes.filter((e=>0===e.depth)).length;const t=this.getParentNode(e);return this.getNodeChildren(t).length}getNodePositionInSet(e){const t=this.getParentNode(e);let i=[];return 0===e.depth?i=this.displayNodes.filter((e=>0===e.depth)):null!==t&&(i=this.getNodeChildren(t)),i.indexOf(e)+1}getFirstNode(){return this.displayNodes.length?this.displayNodes[0]:null}getPreviousNode(e){const t=this.displayNodes.indexOf(e)-1;return this.displayNodes[t]?this.displayNodes[t]:null}getNextNode(e){const t=this.displayNodes.indexOf(e)+1;return this.displayNodes[t]?this.displayNodes[t]:null}getLastNode(){return this.displayNodes.length?this.displayNodes[this.displayNodes.length-1]:null}getParentNode(e){return e.__parents.length?this.getNodeByTreeIdentifier(this.getNodeParentTreeIdentifier(e)):null}getNodeTitle(e){let t=e.tooltip?e.tooltip:"uid="+e.identifier+" "+e.name;const i=this.getNodeStatusInformation(e);return i.length&&(t+="; "+i.map((e=>e.label)).join("; ")),t}handleNodeToggle(e){e.expanded?this.hideChildren(e):this.showChildren(e)}isRTL(){return"rtl"===window.getComputedStyle(document.documentElement).getPropertyValue("direction")}handleKeyboardInteraction(e){if(null!==this.editingNode)return;if(!1===[KeyTypes.ENTER,KeyTypes.SPACE,KeyTypes.END,KeyTypes.HOME,KeyTypes.LEFT,KeyTypes.UP,KeyTypes.RIGHT,KeyTypes.DOWN].includes(e.key))return;const t=e.target,i=this.getNodeFromElement(t);if(null===i)return;const o=this.getParentNode(i),n=this.getFirstNode(),s=this.getPreviousNode(i),r=this.getNextNode(i),d=this.getLastNode();switch(e.preventDefault(),e.key){case KeyTypes.HOME:null!==n&&(this.scrollNodeIntoVisibleArea(n),this.focusNode(n));break;case KeyTypes.END:null!==d&&(this.scrollNodeIntoVisibleArea(d),this.focusNode(d));break;case KeyTypes.UP:null!==s&&(this.scrollNodeIntoVisibleArea(s),this.focusNode(s));break;case KeyTypes.DOWN:null!==r&&(this.scrollNodeIntoVisibleArea(r),this.focusNode(r));break;case KeyTypes.LEFT:i.expanded?i.hasChildren&&this.hideChildren(i):o&&(this.scrollNodeIntoVisibleArea(o),this.focusNode(o));break;case KeyTypes.RIGHT:i.expanded&&r?(this.scrollNodeIntoVisibleArea(r),this.focusNode(r)):i.hasChildren&&this.showChildren(i);break;case KeyTypes.ENTER:case KeyTypes.SPACE:this.selectNode(i)}}scrollNodeIntoVisibleArea(e){const t=e.__y,i=e.__y+this.nodeHeight,o=t>=this.currentScrollPosition,n=i<=this.currentScrollPosition+this.currentVisibleHeight;if(!(o&&n)){let e=this.currentScrollPosition;o||n?o?n||(e=i-this.currentVisibleHeight):e=t:e=i-this.currentVisibleHeight,e<0&&(e=0),this.root.scrollTo({top:e})}}registerUnloadHandler(){try{if(!window.frameElement)return;window.addEventListener("pagehide",(()=>this.muteErrorNotifications=!0),{once:!0})}catch(e){console.error("Failed to check the existence of window.frameElement – using a foreign origin?")}}}__decorate([property({type:Object})],Tree.prototype,"setup",void 0),__decorate([state()],Tree.prototype,"settings",void 0),__decorate([query(".nodes-root")],Tree.prototype,"root",void 0),__decorate([state()],Tree.prototype,"nodes",void 0),__decorate([state()],Tree.prototype,"currentScrollPosition",void 0),__decorate([state()],Tree.prototype,"currentVisibleHeight",void 0),__decorate([state()],Tree.prototype,"searchTerm",void 0),__decorate([state()],Tree.prototype,"loading",void 0),__decorate([state()],Tree.prototype,"hoveredNode",void 0),__decorate([state()],Tree.prototype,"nodeDragAllowed",void 0); \ No newline at end of file + `}async handleNodeEdit(e,t){console.error("The function Tree->handleNodeEdit is not implemented.")}handleNodeDelete(e){console.error("The function Tree->handleNodeDelete is not implemented.")}handleNodeMove(e,t,i){console.error("The function Tree->handleNodeMove is not implemented.")}async handleNodeAdd(e,t,i){console.error("The function Tree->handleNodeAdd is not implemented.")}createNodeContentAction(e){return html`${nothing}`}createDataTransferItemsFromNode(e){throw new Error("The function Tree->createDataTransferItemFromNode is not implemented.")}getNodeIdentifier(e){return e.identifier}getNodeTreeIdentifier(e){return e.__treeIdentifier}getNodeParentTreeIdentifier(e){return e.__parents.join("_")}getNodeClasses(e){const t=["node"];return e.checked&&t.push("node-selected"),this.draggingNode===e&&t.push("node-dragging"),t}getNodeLabels(e){let t=e.labels;if(t.length>0)return t=t.sort(((e,t)=>t.priority-e.priority)),t;const i=this.getParentNode(e);return null===i?[]:this.getNodeLabels(i)}getNodeStatusInformation(e){if(0===e.statusInformation.length)return[];return e.statusInformation.sort(((e,t)=>e.severity!==t.severity?t.severity-e.severity:t.priority-e.priority))}getNodeDepth(e){return e.depth}getNodeChildren(e){return e.hasChildren?this.displayNodes.filter((t=>e===this.getParentNode(t))):[]}getNodeSetsize(e){if(0===e.depth)return this.displayNodes.filter((e=>0===e.depth)).length;const t=this.getParentNode(e);return this.getNodeChildren(t).length}getNodePositionInSet(e){const t=this.getParentNode(e);let i=[];return 0===e.depth?i=this.displayNodes.filter((e=>0===e.depth)):null!==t&&(i=this.getNodeChildren(t)),i.indexOf(e)+1}getFirstNode(){return this.displayNodes.length?this.displayNodes[0]:null}getPreviousNode(e){const t=this.displayNodes.indexOf(e)-1;return this.displayNodes[t]?this.displayNodes[t]:null}getNextNode(e){const t=this.displayNodes.indexOf(e)+1;return this.displayNodes[t]?this.displayNodes[t]:null}getLastNode(){return this.displayNodes.length?this.displayNodes[this.displayNodes.length-1]:null}getParentNode(e){return e.__parents.length?this.getNodeByTreeIdentifier(this.getNodeParentTreeIdentifier(e)):null}getNodeTitle(e){let t=e.tooltip?e.tooltip:"uid="+e.identifier+" "+e.name;const i=this.getNodeLabels(e);i.length&&(t+="; "+i.map((e=>e.label)).join("; "));const o=this.getNodeStatusInformation(e);return o.length&&(t+="; "+o.map((e=>e.label)).join("; ")),t}handleNodeToggle(e){e.expanded?this.hideChildren(e):this.showChildren(e)}isRTL(){return"rtl"===window.getComputedStyle(document.documentElement).getPropertyValue("direction")}handleKeyboardInteraction(e){if(null!==this.editingNode)return;if(!1===[KeyTypes.ENTER,KeyTypes.SPACE,KeyTypes.END,KeyTypes.HOME,KeyTypes.LEFT,KeyTypes.UP,KeyTypes.RIGHT,KeyTypes.DOWN].includes(e.key))return;const t=e.target,i=this.getNodeFromElement(t);if(null===i)return;const o=this.getParentNode(i),n=this.getFirstNode(),s=this.getPreviousNode(i),r=this.getNextNode(i),d=this.getLastNode();switch(e.preventDefault(),e.key){case KeyTypes.HOME:null!==n&&(this.scrollNodeIntoVisibleArea(n),this.focusNode(n));break;case KeyTypes.END:null!==d&&(this.scrollNodeIntoVisibleArea(d),this.focusNode(d));break;case KeyTypes.UP:null!==s&&(this.scrollNodeIntoVisibleArea(s),this.focusNode(s));break;case KeyTypes.DOWN:null!==r&&(this.scrollNodeIntoVisibleArea(r),this.focusNode(r));break;case KeyTypes.LEFT:i.expanded?i.hasChildren&&this.hideChildren(i):o&&(this.scrollNodeIntoVisibleArea(o),this.focusNode(o));break;case KeyTypes.RIGHT:i.expanded&&r?(this.scrollNodeIntoVisibleArea(r),this.focusNode(r)):i.hasChildren&&this.showChildren(i);break;case KeyTypes.ENTER:case KeyTypes.SPACE:this.selectNode(i)}}scrollNodeIntoVisibleArea(e){const t=e.__y,i=e.__y+this.nodeHeight,o=t>=this.currentScrollPosition,n=i<=this.currentScrollPosition+this.currentVisibleHeight;if(!(o&&n)){let e=this.currentScrollPosition;o||n?o?n||(e=i-this.currentVisibleHeight):e=t:e=i-this.currentVisibleHeight,e<0&&(e=0),this.root.scrollTo({top:e})}}registerUnloadHandler(){try{if(!window.frameElement)return;window.addEventListener("pagehide",(()=>this.muteErrorNotifications=!0),{once:!0})}catch(e){console.error("Failed to check the existence of window.frameElement – using a foreign origin?")}}}__decorate([property({type:Object})],Tree.prototype,"setup",void 0),__decorate([state()],Tree.prototype,"settings",void 0),__decorate([query(".nodes-root")],Tree.prototype,"root",void 0),__decorate([state()],Tree.prototype,"nodes",void 0),__decorate([state()],Tree.prototype,"currentScrollPosition",void 0),__decorate([state()],Tree.prototype,"currentVisibleHeight",void 0),__decorate([state()],Tree.prototype,"searchTerm",void 0),__decorate([state()],Tree.prototype,"loading",void 0),__decorate([state()],Tree.prototype,"hoveredNode",void 0),__decorate([state()],Tree.prototype,"nodeDragAllowed",void 0); \ No newline at end of file diff --git a/typo3/sysext/core/Documentation/Changelog/13.1/Deprecation-103211-DeprecatePageTreebackgroundColor.rst b/typo3/sysext/core/Documentation/Changelog/13.1/Deprecation-103211-DeprecatePageTreebackgroundColor.rst new file mode 100644 index 0000000000000000000000000000000000000000..3f4309a9ae81cdb3d7126360b1156be76f5436d0 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.1/Deprecation-103211-DeprecatePageTreebackgroundColor.rst @@ -0,0 +1,54 @@ +.. include:: /Includes.rst.txt + +.. _deprecation-103211-1709038752: + +========================================================= +Deprecation: #103211 - Deprecate pageTree.backgroundColor +========================================================= + +See :issue:`103211` + +Description +=========== + +The user TSconfig option :tsconfig:`options.pageTree.backgroundColor` +has been deprecated and will be removed in TYPO3 v14 due to its +lack of accessibility. It is being replaced with a new label +system for tree nodes. + + +Impact +====== + +During v13, :tsconfig:`options.pageTree.backgroundColor` will be +migrated to the new label system. Since the use case is unknown, +the generated label will be "Color: <value>". This information +will be displayed on all affected nodes. + + +Affected installations +====================== + +All installations that use the user TSconfig option +:tsconfig:`options.pageTree.backgroundColor` are affected. + + +Migration +========= + +Before: + +.. code-block:: tsconfig + + options.pageTree.backgroundColor.<pageid> = #ff8700 + +After: + +.. code-block:: tsconfig + + options.pageTree.label.<pageid> { + label = Campaign A + color = #ff8700 + } + +.. index:: Backend, JavaScript, TSconfig, NotScanned, ext:backend diff --git a/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103211-IntroduceTreeNodeLabels.rst b/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103211-IntroduceTreeNodeLabels.rst new file mode 100644 index 0000000000000000000000000000000000000000..cc4fdcb3a241722f4af2cc9e3398db11f787844a --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.1/Feature-103211-IntroduceTreeNodeLabels.rst @@ -0,0 +1,64 @@ +.. include:: /Includes.rst.txt + +.. _feature-103211-1709036591: + +============================================= +Feature: #103211 - Introduce tree node labels +============================================= + +See :issue:`103211` + +Description +=========== + +We've upgraded the backend tree component by extending tree nodes to +incorporate labels, offering enhanced functionality and additional +information. + +Before the implementation of labels, developers and integrators +relied on :tsconfig:`pageTree.backgroundColor.<pageid>` for visual cues. +However, these background colors lacked accessibility and meaningful context, +catering only to users with perfect eyesight and excluding those +dependent on screen readers or contrast modes. + +With labels, we now cater to all editors. These labels not only offer +customizable color markings for tree nodes but also require an +associated label for improved accessibility. + +Each node can support multiple labels, sorted by priority, with the +highest priority label taking precedence over others. Users can +assign a label to a node via user TSconfig, noting that only one label +can be set through this method. + +.. code-block:: tsconfig + + options.pageTree.label.<pageid> { + label = Campaign A + color = #ff8700 + } + +The labels can also be added by using the event +:php:`\TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent`. + +.. code-block:: php + + $items = $event->getItems(); + foreach ($items as &$item) { + $item['labels'][] = new Label( + label: 'Campaign B', + color: #00658f, + priority: 1, + ); + } + +Please note that only the marker for the label with the highest priority is +rendered. All additional labels will only be added to the title of the node. + + +Impact +====== + +Labels are now added to the node and their children, significantly +improving the clarity and accessibility of the tree component. + +.. index:: Backend, JavaScript, TSconfig, ext:backend diff --git a/typo3/sysext/core/Resources/Private/Language/locallang_core.xlf b/typo3/sysext/core/Resources/Private/Language/locallang_core.xlf index e61876eaaa779a7968fd302187c3e72f784240a4..0b0fce56a61e7db3172e70c93be9392abe2a641b 100644 --- a/typo3/sysext/core/Resources/Private/Language/locallang_core.xlf +++ b/typo3/sysext/core/Resources/Private/Language/locallang_core.xlf @@ -33,6 +33,9 @@ <trans-unit id="labels.lockedRecordUser_content" resname="labels.lockedRecordUser_content"> <source>The %s '%s' began to edit content on this page %s ago.</source> </trans-unit> + <trans-unit id="labels.color" resname="labels.color"> + <source>Color</source> + </trans-unit> <trans-unit id="labels.user" resname="labels.user"> <source>User</source> </trans-unit> diff --git a/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/CategoryTreeCest.php b/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/CategoryTreeCest.php index a3def3e233ea8d1a2d98e460be1c18882c2ab702..25f7142ba91527a8ce6b21eb2d3e226ea94be0bb 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/CategoryTreeCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/FormEngine/CategoryTreeCest.php @@ -59,8 +59,8 @@ final class CategoryTreeCest // Change title and level to root $I->fillField('input[data-formengine-input-name="data[sys_category][7][title]"]', 'level-1-4'); - $I->click('.tree-wrapper [role="treeitem"][data-id="7"] .node-label'); - $I->click('.tree-wrapper [role="treeitem"][data-id="3"] .node-label'); + $I->click('.tree-wrapper [role="treeitem"][data-id="7"] .node-contentlabel'); + $I->click('.tree-wrapper [role="treeitem"][data-id="3"] .node-contentlabel'); $I->click('button[name="_savedok"]'); // Wait for tree and check if isset level-1-4 $I->waitForElement('.tree-wrapper .nodes-list'); diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ExportCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ExportCest.php index 847c8ed8626f2756c285469b2138145c3bdf5382..c7215b131f2aaf67af80ae7ef565c20a5bd08b6b 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ExportCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/ExportCest.php @@ -195,7 +195,7 @@ final class ExportCest extends AbstractCest public function exportTable(ApplicationTester $I): void { - $rootPage = '#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-label'; + $rootPage = '#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-contentlabel'; $rootPageTitle = 'New TYPO3 site'; $beUsergroupTableTitle = 'Backend usergroup'; $listModuleHeader = '.module-docheader'; @@ -231,7 +231,7 @@ final class ExportCest extends AbstractCest public function exportRecord(ApplicationTester $I): void { - $rootPage = '#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-label'; + $rootPage = '#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-contentlabel'; $rootPageTitle = 'New TYPO3 site'; $sysLanguageTable = '#recordlist-be_groups'; $sysLanguageIcon = 'tr:first-child a[data-contextmenu-trigger]'; diff --git a/typo3/sysext/core/Tests/Acceptance/Application/IndexedSearch/IndexedSearchModuleCest.php b/typo3/sysext/core/Tests/Acceptance/Application/IndexedSearch/IndexedSearchModuleCest.php index f62d368305beec99dc44e8bfc587677e85a76f1c..4a9e0417292cad817bace2f5c7e4bd19777831e2 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/IndexedSearch/IndexedSearchModuleCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/IndexedSearch/IndexedSearchModuleCest.php @@ -33,7 +33,7 @@ final class IndexedSearchModuleCest { $I->click('[data-modulemenu-identifier="web_IndexedSearchIsearch"]'); // click on PID=0 - $I->clickWithLeftButton('#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-label'); + $I->clickWithLeftButton('#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-contentlabel'); $I->switchToContentFrame(); $I->seeElement('.t3-js-jumpMenuBox'); $I->selectOption('.t3-js-jumpMenuBox', 'General statistics'); diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Page/PageModuleCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Page/PageModuleCest.php index a9ffda6995319bd5084b7ccf14e63cc32866ef0f..ec9a7f478f4393a3d21e112f1119f6452df09f25 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Page/PageModuleCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Page/PageModuleCest.php @@ -37,7 +37,7 @@ final class PageModuleCest $I->switchToMainFrame(); $I->click('Page'); // click on PID=0 - $I->clickWithLeftButton('#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-label'); + $I->clickWithLeftButton('#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-contentlabel'); $I->switchToContentFrame(); $I->canSee('Please select a page in the page tree to edit page content.'); } diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php index 0d13e77f9bbd33a1d0fc8b5ebce5f5cf965b4b89..bc37fa0239c5037ca590bf60820d6b28aca6c143 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Template/TemplateCest.php @@ -38,7 +38,7 @@ final class TemplateCest // Select the root page $I->switchToMainFrame(); // click on PID=0 - $I->clickWithLeftButton('#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-label'); + $I->clickWithLeftButton('#typo3-pagetree-treeContainer [role="treeitem"][data-id="0"] .node-contentlabel'); $I->switchToContentFrame(); $I->waitForElementVisible('#ts-overview'); diff --git a/typo3/sysext/core/Tests/Acceptance/Support/Helper/AbstractTree.php b/typo3/sysext/core/Tests/Acceptance/Support/Helper/AbstractTree.php index 248292449bc6f2b02cd7f19c5cdcceee84b15873..787c488bc4692592c300944fd9cff16c2d79b9e0 100644 --- a/typo3/sysext/core/Tests/Acceptance/Support/Helper/AbstractTree.php +++ b/typo3/sysext/core/Tests/Acceptance/Support/Helper/AbstractTree.php @@ -27,7 +27,7 @@ abstract class AbstractTree // Selectors public static $treeSelector = ''; public static $treeItemSelector = '.nodes-list > [role="treeitem"]'; - public static $treeItemAnchorSelector = '.node-label'; + public static $treeItemAnchorSelector = '.node-contentlabel'; /** * @var \AcceptanceTester