Commit b2fb60f4 authored by Benni Mack's avatar Benni Mack Committed by Richard Haeser
Browse files

[TASK] Clean up SvgTree code

Now that jQuery has gone and TypeScript is in place for Tree components,
the next step is to consolidate and sort out re-usability.

This change prepares the way for allowing multiple trees in the
navigation component area (for FileStorageTree).

* ContextMenu allows int+number (which is correct)
* ContextMenu has all information from the TreeNode (and not the SVG
  element) - less Markup!
* KeyTypeEnums are used throughout SvgTree
* Centralized place for error notifications, removing duplicate code
* tree.settings.isDragAnDrop is removed in favor of
  tree.settings.allowDragMove (which already exists)
* Unused properties in TreeNode are removed
* Since we are now in "TS module land", PageTreeToolbar.ts is removed
  because the class is only used in the PageTreeElement, so it does
  not need to be exposed anymore.

Resolves: #93481
Releases: master
Change-Id: Ia9bff532132dba5aea85352d7e510f6a7f9459c6
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67683


Tested-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
Reviewed-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
parent 813d656a
......@@ -23,7 +23,7 @@ interface MousePosition {
}
interface ActiveRecord {
uid: number;
uid: number|string;
table: string;
}
......@@ -127,7 +127,7 @@ class ContextMenu {
* @param {string} addParams Additional params
* @param {Element} eventSource Source Element
*/
public show(table: string, uid: number, context: string, enDisItems: string, addParams: string, eventSource: Element = null): void {
public show(table: string, uid: number|string, context: string, enDisItems: string, addParams: string, eventSource: Element = null): void {
this.record = {table: table, uid: uid};
// fix: [tabindex=-1] is not focusable!!!
const focusableSource = eventSource.matches('a, button, [tabindex]') ? eventSource : eventSource.closest('a, button, [tabindex]');
......
......@@ -12,6 +12,7 @@
*/
export enum KeyTypesEnum {
TAB = 9,
ENTER = 13,
ESCAPE = 27,
SPACE = 32,
......
......@@ -17,11 +17,11 @@ import {SvgTree, SvgTreeSettings, TreeNodeSelection} from '../SvgTree';
import {TreeNode} from '../Tree/TreeNode';
import {PageTreeDragDrop, PageTreeNodeDragHandler} from './PageTreeDragDrop';
import Icons = require('../Icons');
import Notification = require('../Notification');
import ContextMenu = require('../ContextMenu');
import Persistent from '../Storage/Persistent';
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import {TreeInterface} from '../Viewport/TreeInterface';
import {KeyTypesEnum as KeyTypes} from '../Enum/KeyTypes';
interface PageTreeSettings extends SvgTreeSettings {
temporaryMountPoint?: string;
......@@ -30,16 +30,18 @@ interface PageTreeSettings extends SvgTreeSettings {
export class PageTree extends SvgTree implements TreeInterface
{
public settings: PageTreeSettings;
protected networkErrorTitle: string = TYPO3.lang.pagetree_networkErrorTitle;
protected networkErrorMessage: string = TYPO3.lang.pagetree_networkErrorDesc;
private originalNodes: string = '';
private searchQuery: string = '';
private dragDrop: PageTreeDragDrop;
private nodeIsEdit: boolean;
public constructor() {
super();
this.settings.defaultProperties = {
hasChildren: false,
nameSourceField: 'title',
itemType: 'pages',
prefix: '',
suffix: '',
locked: false,
......@@ -61,10 +63,8 @@ export class PageTree extends SvgTree implements TreeInterface
return false;
}
this.settings.isDragAnDrop = settings.allowDragMove;
this.dispatch.on('nodeSelectedAfter.pageTree', (node: TreeNode) => this.nodeSelectedAfter(node));
this.dispatch.on('nodeRightClick.pageTree', (node: TreeNode) => this.nodeRightClick(node));
this.dispatch.on('updateSvg.pageTree', (node: TreeNode) => this.updateSvg(node));
this.dispatch.on('prepareLoadedNode.pageTree', (node: TreeNode) => this.prepareLoadedNode(node));
this.dragDrop = dragDrop;
......@@ -114,17 +114,7 @@ export class PageTree extends SvgTree implements TreeInterface
})
.then((response) => {
if (response && response.hasErrors) {
if (response.messages) {
response.messages.forEach((message: any) => {
Notification.error(
message.title,
message.message
);
});
} else {
this.errorNotification();
}
this.errorNotification(response.messages, false);
this.nodesContainer.selectAll('.node').remove();
this.update();
this.nodesRemovePlaceholder();
......@@ -142,27 +132,16 @@ export class PageTree extends SvgTree implements TreeInterface
}
public nodeRightClick(node: TreeNode): void {
let svgElement = this.svg.node().querySelector('.nodes .node[data-state-id="' + node.stateIdentifier + '"]') as SVGElement;
if (svgElement) {
ContextMenu.show(
svgElement.dataset.table,
parseInt(node.identifier, 10),
svgElement.dataset.context,
svgElement.dataset.iteminfo,
svgElement.dataset.parameters,
svgElement
);
}
ContextMenu.show(
node.itemType,
parseInt(node.identifier, 10),
'tree',
'',
'',
this.getNodeElement(node)
);
};
public updateSvg(nodeEnter: TreeNode) {
nodeEnter
.select('use')
.attr('data-table', 'pages')
.attr('data-context', 'tree');
}
/**
* Event listener called for each loaded node,
* here used to mark node remembered in fsMode as selected
......@@ -221,17 +200,7 @@ export class PageTree extends SvgTree implements TreeInterface
}
})
.catch((error: any) => {
let title = TYPO3.lang.pagetree_networkErrorTitle;
let desc = TYPO3.lang.pagetree_networkErrorDesc;
if (error && error.target && (error.target.status || error.target.statusText)) {
title += ' - ' + (error.target.status || '') + ' ' + (error.target.statusText || '');
}
Notification.error(
title,
desc);
this.errorNotification(error, false)
this.nodesRemovePlaceholder();
throw error;
});
......@@ -242,14 +211,7 @@ export class PageTree extends SvgTree implements TreeInterface
};
public nodesUpdate(nodes: TreeNodeSelection) {
nodes = super.nodesUpdate.call(this, nodes)
.call(this.initializeDragForNode())
.attr('data-table', 'pages')
.attr('data-context', 'tree')
.on('contextmenu', (evt: MouseEvent, node: TreeNode) => {
evt.preventDefault();
this.dispatch.call('nodeRightClick', this, node);
});
nodes = super.nodesUpdate.call(this, nodes).call(this.initializeDragForNode());
nodes
.append('text')
......@@ -304,14 +266,7 @@ export class PageTree extends SvgTree implements TreeInterface
this.nodesRemovePlaceholder();
})
.catch((error: any) => {
let title = TYPO3.lang.pagetree_networkErrorTitle;
const desc = TYPO3.lang.pagetree_networkErrorDesc;
if (error && error.target && (error.target.status || error.target.statusText)) {
title += ' - ' + (error.target.status || '') + ' ' + (error.target.statusText || '');
}
Notification.error(title, desc);
this.errorNotification(error, false)
this.nodesRemovePlaceholder();
throw error;
});
......@@ -359,17 +314,7 @@ export class PageTree extends SvgTree implements TreeInterface
})
.then((response) => {
if (response && response.hasErrors) {
if (response.messages) {
response.messages.forEach((message: any) => {
Notification.error(
message.title,
message.message
);
});
} else {
this.errorNotification();
}
this.errorNotification(response.message, true);
this.update();
} else {
this.addMountPoint(response.mountPointPath);
......@@ -377,7 +322,7 @@ export class PageTree extends SvgTree implements TreeInterface
}
})
.catch((error) => {
this.errorNotification(error);
this.errorNotification(error, true);
});
}
......@@ -403,7 +348,6 @@ export class PageTree extends SvgTree implements TreeInterface
* Changed text position if there is 'stop page tree' option
*/
protected appendTextElement(nodes: TreeNodeSelection): TreeNodeSelection {
let clicks = 0;
return super.appendTextElement(nodes)
.attr('dx', (node) => {
......@@ -434,20 +378,6 @@ export class PageTree extends SvgTree implements TreeInterface
});
};
private removeNode(newNode: any) {
let index = this.nodes.indexOf(newNode);
// if newNode is only one child
if (this.nodes[index - 1].depth != newNode.depth
&& (!this.nodes[index + 1] || this.nodes[index + 1].depth != newNode.depth)) {
this.nodes[index - 1].hasChildren = false;
}
this.nodes.splice(index, 1);
this.setParametersNode();
this.prepareDataForVisibleNodes();
this.update();
this.removeEditedText();
};
private sendEditNodeLabelCommand(node: TreeNode) {
const params = '&data[pages][' + node.identifier + '][' + node.nameSourceField + ']=' + encodeURIComponent(node.newName);
......@@ -463,28 +393,14 @@ export class PageTree extends SvgTree implements TreeInterface
})
.then((response) => {
if (response && response.hasErrors) {
if (response.messages) {
response.messages.forEach((message: any) => {
Notification.error(
message.title,
message.message
);
});
} else {
this.errorNotification();
}
this.nodesAddPlaceholder();
this.refreshOrFilterTree();
this.errorNotification(response.messages, false);
} else {
node.name = node.newName;
this.svg.select('.node-placeholder[data-uid="' + node.stateIdentifier + '"]').remove();
this.refreshOrFilterTree();
this.nodesRemovePlaceholder();
}
this.refreshOrFilterTree();
})
.catch((error) => {
this.errorNotification(error);
this.errorNotification(error, true);
});
}
......@@ -492,8 +408,6 @@ export class PageTree extends SvgTree implements TreeInterface
if (!node.allowEdit) {
return;
}
const _this = this;
this.removeEditedText();
this.nodeIsEdit = true;
......@@ -509,24 +423,23 @@ export class PageTree extends SvgTree implements TreeInterface
.style('height', this.settings.nodeHeight + 'px')
.attr('type', 'text')
.attr('value', node.name)
.on('keydown', function(this: HTMLInputElement, event: KeyboardEvent) {
.on('keydown', (event: KeyboardEvent) => {
// @todo Migrate to `evt.code`, see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
const code = event.keyCode;
if (code === 13 || code === 9) { //enter || tab
const newName = this.value.trim();
if (code === KeyTypes.ENTER || code === KeyTypes.TAB) {
const target = event.target as HTMLInputElement;
const newName = target.value.trim();
this.nodeIsEdit = false;
this.removeEditedText();
if (newName.length && (newName !== node.name)) {
_this.nodeIsEdit = false;
_this.removeEditedText();
node.nameSourceField = node.nameSourceField || 'title';
node.newName = newName;
_this.sendEditNodeLabelCommand(node);
} else {
_this.nodeIsEdit = false;
_this.removeEditedText();
this.sendEditNodeLabelCommand(node);
}
} else if (code === 27) { //esc
_this.nodeIsEdit = false;
_this.removeEditedText();
} else if (code === KeyTypes.ESCAPE) {
this.nodeIsEdit = false;
this.removeEditedText();
}
})
.on('blur', (evt: FocusEvent) => {
......@@ -616,20 +529,5 @@ export class PageTree extends SvgTree implements TreeInterface
iconElement.insertAdjacentHTML('beforeend', icon);
});
});
};
/**
* Displays a notification message and refresh nodes
*/
private errorNotification(error: any = null): void {
let title = TYPO3.lang.pagetree_networkErrorTitle;
const desc = TYPO3.lang.pagetree_networkErrorDesc;
if (error && error.target && (error.target.status || error.target.statusText)) {
title += ' - ' + (error.target.status || '') + ' ' + (error.target.statusText || '');
}
Notification.error(title, desc);
this.loadData();
}
}
......@@ -169,7 +169,7 @@ export class ToolbarDragHandler implements DragDropHandler {
this.isDragged = false;
this.dragDrop.removeNodeDdClass();
if (this.tree.settings.isDragAnDrop !== true || !this.tree.hoveredNode || !this.tree.isOverSvg) {
if (this.tree.settings.allowDragMove !== true || !this.tree.hoveredNode || !this.tree.isOverSvg) {
return false;
}
if (this.tree.settings.canNodeDrag) {
......@@ -260,11 +260,11 @@ export class ToolbarDragHandler implements DragDropHandler {
this.tree.removeEditedText();
this.tree.sendChangeCommand(newNode);
} else {
this.tree.removeNode(newNode);
this.removeNode(newNode);
}
} else if (code === 27) { // esc
this.tree.nodeIsEdit = false;
this.tree.removeNode(newNode);
this.removeNode(newNode);
}
})
.on('blur', (evt: FocusEvent) => {
......@@ -276,7 +276,7 @@ export class ToolbarDragHandler implements DragDropHandler {
this.tree.removeEditedText();
this.tree.sendChangeCommand(newNode);
} else {
this.tree.removeNode(newNode);
this.removeNode(newNode);
}
}
})
......@@ -284,6 +284,19 @@ export class ToolbarDragHandler implements DragDropHandler {
.select();
}
private removeNode(newNode: TreeNode) {
let index = this.tree.nodes.indexOf(newNode);
// if newNode is only one child
if (this.tree.nodes[index - 1].depth != newNode.depth
&& (!this.tree.nodes[index + 1] || this.tree.nodes[index + 1].depth != newNode.depth)) {
this.tree.nodes[index - 1].hasChildren = false;
}
this.tree.nodes.splice(index, 1);
this.tree.setParametersNode();
this.tree.prepareDataForVisibleNodes();
this.tree.update();
this.tree.removeEditedText();
};
}
/**
......@@ -312,7 +325,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
public dragStart(event: TreeNodeDragEvent): boolean {
const node = event.subject;
if (this.tree.settings.isDragAnDrop !== true || node.depth === 0) {
if (this.tree.settings.allowDragMove !== true || node.depth === 0) {
return false;
}
this.dropZoneDelete = null;
......@@ -359,7 +372,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
return false;
}
if (this.tree.settings.isDragAnDrop !== true || node.depth === 0) {
if (this.tree.settings.allowDragMove !== true || node.depth === 0) {
return false;
}
......@@ -414,7 +427,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
this.dropZoneDelete = null;
}
if (!this.startDrag || this.tree.settings.isDragAnDrop !== true || node.depth === 0) {
if (!this.startDrag || this.tree.settings.allowDragMove !== true || node.depth === 0) {
return false;
}
......
......@@ -13,13 +13,15 @@
import {render} from 'lit-html';
import {html, TemplateResult} from 'lit-element';
import {icon} from 'TYPO3/CMS/Core/lit-helper';
import {icon, lll} from 'TYPO3/CMS/Core/lit-helper';
import {PageTree} from './PageTree';
import {PageTreeDragDrop} from './PageTreeDragDrop';
import {PageTreeDragDrop, ToolbarDragHandler} from './PageTreeDragDrop';
import viewPort from '../Viewport';
import {PageTreeToolbar} from './PageTreeToolbar';
import AjaxRequest from 'TYPO3/CMS/Core/Ajax/AjaxRequest';
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import {select as d3select} from 'd3-selection';
import DebounceEvent from 'TYPO3/CMS/Core/Event/DebounceEvent';
import {SvgTreeWrapper} from '../SvgTree';
/**
* @exports TYPO3/CMS/Backend/PageTree/PageTreeElement
......@@ -55,7 +57,7 @@ export class PageTreeElement {
// the toolbar relies on settings retrieved in this step
const toolbar = <HTMLElement>targetEl.querySelector('.svg-toolbar');
if (!toolbar.dataset.treeShowToolbar) {
const pageTreeToolbar = new PageTreeToolbar(dragDrop);
const pageTreeToolbar = new Toolbar(dragDrop);
pageTreeToolbar.initialize(treeEl, toolbar);
toolbar.dataset.treeShowToolbar = 'true';
}
......@@ -82,3 +84,123 @@ export class PageTreeElement {
`;
}
}
class Toolbar {
private settings = {
toolbarSelector: 'tree-toolbar',
searchInput: '.search-input',
filterTimeout: 450
};
private treeContainer: SvgTreeWrapper;
private targetEl: HTMLElement;
private tree: any;
private readonly dragDrop: any;
public constructor(dragDrop: PageTreeDragDrop) {
this.dragDrop = dragDrop;
}
public initialize(treeContainer: HTMLElement, toolbar: HTMLElement, settings: any = {}): void {
this.treeContainer = treeContainer;
this.targetEl = toolbar;
if (!this.treeContainer.dataset.svgTreeInitialized
|| typeof this.treeContainer.svgtree !== 'object'
) {
//both toolbar and tree are loaded independently through require js,
//so we don't know which is loaded first
//in case of toolbar being loaded first, we wait for an event from svgTree
this.treeContainer.addEventListener('svg-tree:initialized', () => this.render());
return;
}
Object.assign(this.settings, settings);
this.render();
}
private refreshTree(): void {
this.tree.refreshOrFilterTree();
}
private search(inputEl: HTMLInputElement): void {
this.tree.searchQuery = inputEl.value.trim()
this.tree.refreshOrFilterTree();
this.tree.prepareDataForVisibleNodes();
this.tree.update();
}
private render(): void
{
this.tree = this.treeContainer.svgtree;
// @todo Better use initialize() settings, drop this assignment here
Object.assign(this.settings, this.tree.settings);
render(this.renderTemplate(), this.targetEl);
const d3Toolbar = d3select('.svg-toolbar');
this.tree.settings.doktypes.forEach((item: any) => {
if (item.icon) {
d3Toolbar
.selectAll('[data-tree-icon=' + item.icon + ']')
.call(this.dragToolbar(item));
} else {
console.warn('Missing icon definition for doktype: ' + item.nodeType);
}
});
const inputEl = this.targetEl.querySelector(this.settings.searchInput) as HTMLInputElement;
if (inputEl) {
new DebounceEvent('input', (evt: InputEvent) => {
this.search(evt.target as HTMLInputElement);
}, this.settings.filterTimeout).bindTo(inputEl);
inputEl.focus();
inputEl.clearable({
onClear: () => {
this.tree.resetFilter();
this.tree.prepareDataForVisibleNodes();
this.tree.update();
}
});
}
}
private renderTemplate(): TemplateResult {
/* eslint-disable @typescript-eslint/indent */
return html`
<div class="${this.settings.toolbarSelector}">
<div class="svg-toolbar__menu">
<div class="svg-toolbar__search">
<input type="text" class="form-control form-control-sm search-input" placeholder="${lll('tree.searchTermInfo')}">
</div>
<button class="btn btn-default btn-borderless btn-sm" @click="${() => this.refreshTree()}" data-tree-icon="actions-refresh" title="${lll('labels.refresh')}">
${icon('actions-refresh', 'small')}
</button>
</div>
<div class="svg-toolbar__submenu">
${this.tree.settings.doktypes && this.tree.settings.doktypes.length
? this.tree.settings.doktypes.map((item: any) => {
// @todo Unsure, why this has to be done for doktype icons
this.tree.fetchIcon(item.icon, false);
return html`
<div class="svg-toolbar__drag-node" data-tree-icon="${item.icon}" data-node-type="${item.nodeType}"
title="${item.title}" tooltip="${item.tooltip}">
${icon(item.icon, 'small')}
</div>
`;
})
: ''
}
</div>
</div>
`;
}
/**
* Register Drag and drop for new elements of toolbar
* Returns method from d3drag
*/
private dragToolbar(item: any) {
return this.dragDrop.connectDragHandler(new ToolbarDragHandler(item, this.tree, this.dragDrop));
}
}
/*
* 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