From 93e6d6d8b7e6db7489e84d8762db979e8502f866 Mon Sep 17 00:00:00 2001 From: Benjamin Kott <benjamin.kott@outlook.com> Date: Tue, 27 Feb 2024 14:08:17 +0100 Subject: [PATCH] [FEATURE] Introduce tree node labels 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 `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 tsconfig, noting that only one label can be set through this method. ``` options.pageTree.label.<pageid> { label = Campaign A color = #ff8700 } ``` The labels can also be added by using the event `\TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent`. ``` $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. Resolves: #103211 Releases: main Change-Id: I5daaa1efe6e11c506bd5f5f86770bf9895bb6789 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83143 Tested-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Oliver Bartsch <bo@cedev.de> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Benjamin Kott <benjamin.kott@outlook.com> Tested-by: Benjamin Kott <benjamin.kott@outlook.com> Tested-by: Benjamin Franzke <ben@bnf.dev> Reviewed-by: Benjamin Franzke <ben@bnf.dev> --- Build/Sources/Sass/component/_tree.scss | 13 +++- .../backend/tree/page-tree-element.ts | 1 + .../TypeScript/backend/tree/tree-node.ts | 7 ++ Build/Sources/TypeScript/backend/tree/tree.ts | 46 ++++++++++++- .../Controller/FileStorage/TreeController.php | 1 + .../FormSelectTreeAjaxController.php | 1 + .../Controller/Page/TreeController.php | 35 ++++++++++ .../backend/Classes/Dto/Tree/Label/Label.php | 35 ++++++++++ .../backend/Classes/Dto/Tree/TreeItem.php | 3 + .../backend/Resources/Public/Css/backend.css | 3 +- .../JavaScript/tree/page-tree-element.js | 2 +- .../Resources/Public/JavaScript/tree/tree.js | 9 ++- ...03211-DeprecatePageTreebackgroundColor.rst | 54 ++++++++++++++++ ...Feature-103211-IntroduceTreeNodeLabels.rst | 64 +++++++++++++++++++ .../Private/Language/locallang_core.xlf | 3 + .../FormEngine/CategoryTreeCest.php | 4 +- .../Application/Impexp/ExportCest.php | 4 +- .../IndexedSearch/IndexedSearchModuleCest.php | 2 +- .../Application/Page/PageModuleCest.php | 2 +- .../Application/Template/TemplateCest.php | 2 +- .../Support/Helper/AbstractTree.php | 2 +- 21 files changed, 276 insertions(+), 17 deletions(-) create mode 100644 typo3/sysext/backend/Classes/Dto/Tree/Label/Label.php create mode 100644 typo3/sysext/core/Documentation/Changelog/13.1/Deprecation-103211-DeprecatePageTreebackgroundColor.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/13.1/Feature-103211-IntroduceTreeNodeLabels.rst diff --git a/Build/Sources/Sass/component/_tree.scss b/Build/Sources/Sass/component/_tree.scss index 0a3093a5c453..bd024a63941e 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 3639a14832c1..a1257d83ecba 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 7c3a0a1a4576..2a789391ddd9 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 21a2b6fa8bb1..05ee45c52796 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 5343b1e0ce17..5c02722c685e 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 f2e8826dc0f6..fe8c8588c724 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 65227c5e4a59..be5f5719b484 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 000000000000..219ece9b6981 --- /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 0d376f115886..65a5a3694317 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 3ea3e7090ae8..5be4e5c3e167 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 15e9e7094ec2..e428485036af 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 536322e82f53..2b6cc293060f 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 000000000000..3f4309a9ae81 --- /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 000000000000..cc4fdcb3a241 --- /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 e61876eaaa77..0b0fce56a61e 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 a3def3e233ea..25f7142ba915 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 847c8ed8626f..c7215b131f2a 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 f62d368305be..4a9e0417292c 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 a9ffda699531..ec9a7f478f43 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 0d13e77f9bbd..bc37fa0239c5 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 248292449bc6..787c488bc469 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 -- GitLab