Commit 478c69cb authored by Benni Mack's avatar Benni Mack Committed by Georg Ringer
Browse files

[TASK] Migrate SvgTree items to TypeScript

This change migrates all left-over components of
* SvgTree
* SelectTree (a.k.a. CategoryTree)
* and PageTree

to TypeScript adding types in various places
to make the code less error-prone.

Resolves: #87678
Resolves: #85400
Releases: master
Change-Id: Ibb4024d365857c44018a7a201c1cf75baa48179a
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67662


Tested-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
parent 8330dabb
......@@ -95,10 +95,6 @@ class ContextMenu {
}
constructor() {
this.initializeEvents();
}
private initializeEvents(): void {
$(document).on('click contextmenu', '.t3js-contextmenutrigger', (e: JQueryEventObject): void => {
const $me = $(e.currentTarget);
// if there is an other "inline" onclick setting, context menu is not triggered
......@@ -131,7 +127,7 @@ class ContextMenu {
* @param {string} addParams Additional params
* @param {Element} eventSource Source Element
*/
private show(table: string, uid: number, context: string, enDisItems: string, addParams: string, eventSource: Element = null): void {
public show(table: string, uid: number, 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]');
......
/*
* 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!
*/
export enum Sizes {
small = 'small',
default = 'default',
large = 'large',
overlay = 'overlay',
}
export enum States {
default = 'default',
disabled = 'disabled',
}
export enum MarkupIdentifiers {
default = 'default',
inline = 'inline',
}
/*
* 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!
*/
import * as d3selection from 'd3-selection';
import {SvgTree, TreeNodeSelection} from '../../SvgTree';
import {TreeNode} from '../../Tree/TreeNode';
import FormEngineValidation = require('TYPO3/CMS/Backend/FormEngineValidation');
export class SelectTree extends SvgTree
{
public constructor() {
super();
this.settings.showCheckboxes = true;
}
/**
* SelectTree initialization
*
* @param {HTMLElement} selector
* @param {Object} settings
*/
public initialize(selector: HTMLElement, settings: any): boolean {
if (!super.initialize(selector, settings)) {
return false;
}
this.addIcons();
this.dispatch.on('updateNodes.selectTree', (nodes: TreeNodeSelection) => this.updateNodes(nodes));
this.dispatch.on('loadDataAfter.selectTree', () => this.loadDataAfter());
this.dispatch.on('updateSvg.selectTree', (nodes: TreeNodeSelection) => this.renderCheckbox(nodes));
this.dispatch.on('nodeSelectedAfter.selectTree', (node: TreeNode) => this.nodeSelectedAfter(node));
return true;
};
/**
* Function relays on node.indeterminate state being up to date
*
* @param {Selection} nodes
*/
public updateNodes(nodes: TreeNodeSelection): void {
if (this.settings.showCheckboxes) {
return;
}
nodes
.selectAll('.tree-check use')
.attr('visibility', function(this: SVGUseElement, node: TreeNode): string {
const checked = Boolean(node.checked);
const selection = d3selection.select(this);
if (selection.classed('icon-checked') && checked) {
return 'visible';
} else if (selection.classed('icon-indeterminate') && node.indeterminate && !checked) {
return 'visible';
} else if (selection.classed('icon-check') && !node.indeterminate && !checked) {
return 'visible';
} else {
return 'hidden';
}
});
}
/**
* Adds svg elements for checkbox rendering.
*
* @param {Selection} nodeSelection ENTER selection (only new DOM objects)
*/
private renderCheckbox(nodeSelection: TreeNodeSelection): void {
if (this.settings.showCheckboxes) {
this.textPosition = 50;
// this can be simplified to single "use" element with changing href on click
// when we drop IE11 on WIN7 support
const g = nodeSelection.filter((node: TreeNode) => {
// do not render checkbox if node is not selectable
return this.isNodeSelectable(node) || Boolean(node.checked);
})
.append('g')
.attr('class', 'tree-check')
.on('click', (evt: MouseEvent, node: TreeNode) => this.selectNode(node));
g.append('use')
.attr('x', 28)
.attr('y', -8)
.attr('visibility', 'hidden')
.attr('class', 'icon-check')
.attr('xlink:href', '#icon-check');
g.append('use')
.attr('x', 28)
.attr('y', -8)
.attr('visibility', 'hidden')
.attr('class', 'icon-checked')
.attr('xlink:href', '#icon-checked');
g.append('use')
.attr('x', 28)
.attr('y', -8)
.attr('visibility', 'hidden')
.attr('class', 'icon-indeterminate')
.attr('xlink:href', '#icon-indeterminate');
}
};
/**
* Updates the indeterminate state for ancestors of the current node
*/
private updateAncestorsIndeterminateState(node: TreeNode): void {
// foreach ancestor except node itself
let indeterminate = false;
node.parents.forEach((index: number) => {
const node = this.nodes[index];
node.indeterminate = (node.checked || node.indeterminate || indeterminate);
// check state for the next level
indeterminate = (node.checked || node.indeterminate || node.checked || node.indeterminate);
});
};
/**
* Resets the node.indeterminate for the whole tree.
* It's done once after loading data.
* Later indeterminate state is updated just for the subset of nodes
*/
private loadDataAfter(): void {
this.nodes = this.nodes.map((node: TreeNode) => {
node.indeterminate = false;
return node;
});
this.calculateIndeterminate(this.nodes);
// Initialise "value" attribute of input field after load and revalidate form engine fields
this.saveCheckboxes();
// @todo Unsure if this has ever worked before from `TYPO3.FormEngine.Validation`
FormEngineValidation.validateField(this.settings.input);
};
/**
* Sets indeterminate state for a subtree.
* It relays on the tree to have indeterminate state reset beforehand.
*/
private calculateIndeterminate(nodes: TreeNode[]): void {
nodes.forEach((node: TreeNode) => {
if ((node.checked || node.indeterminate) && node.parents && node.parents.length > 0) {
node.parents.forEach((parentNodeIndex: number) => {
nodes[parentNodeIndex].indeterminate = true;
});
}
});
};
/**
* Observer for the selectedNode event
*/
private nodeSelectedAfter(node: TreeNode): void {
this.updateAncestorsIndeterminateState(node);
// check all nodes again, to ensure correct display of indeterminate state
this.calculateIndeterminate(this.nodes);
this.saveCheckboxes();
};
/**
* Sets a comma-separated list of selected nodes identifiers to configured input
*/
private saveCheckboxes(): void {
if (typeof this.settings.input === 'undefined') {
return;
}
this.settings.input.value = this.getSelectedNodes()
.map((node: TreeNode): string => node.identifier);
}
/**
* Add icons imitating checkboxes
*/
private addIcons(): void {
this.icons = {
check: {
identifier: 'check',
icon: '<g width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">' +
'<rect height="16" width="16" fill="transparent"></rect><path transform="scale(0.01)" d="M1312 256h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-832q0-66-47-113t-113-47zm288 160v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z"></path></g>'
},
checked: {
identifier: 'checked',
icon: '<g width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect height="16" width="16" fill="transparent"></rect><path transform="scale(0.01)" d="M813 1299l614-614q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-467 467-211-211q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l358 358q19 19 45 19t45-19zm851-883v960q0 119-84.5 203.5t-203.5 84.5h-960q-119 0-203.5-84.5t-84.5-203.5v-960q0-119 84.5-203.5t203.5-84.5h960q119 0 203.5 84.5t84.5 203.5z"></path></g>'
},
indeterminate: {
identifier: 'indeterminate',
icon: '<g width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect height="16" width="16" fill="transparent"></rect><path transform="scale(0.01)" d="M1344 800v64q0 14-9 23t-23 9h-832q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h832q14 0 23 9t9 23zm128 448v-832q0-66-47-113t-113-47h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113zm128-832v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z"></path></g>'
}
}
}
}
......@@ -11,28 +11,20 @@
* The TYPO3 project - inspiring people to share!
*/
import SelectTree = require('TYPO3/CMS/Backend/FormEngine/Element/SelectTree');
import {SelectTree} from './SelectTree';
import {TreeToolbar} from './TreeToolbar';
class SelectTreeElement {
export class SelectTreeElement {
private readonly treeWrapper: HTMLElement = null;
private readonly recordField: HTMLInputElement = null;
private readonly callback: Function = null;
constructor(treeWrapperId: string, treeRecordFieldId: string, callback: Function) {
this.treeWrapper = <HTMLElement>document.getElementById(treeWrapperId);
this.recordField = <HTMLInputElement>document.getElementById(treeRecordFieldId);
this.callback = callback;
this.initialize();
}
private initialize(): void {
const dataUrl = this.generateRequestUrl();
const tree = new SelectTree();
const settings = {
dataUrl: dataUrl,
dataUrl: this.generateRequestUrl(),
showIcons: true,
showCheckboxes: true,
readOnlyMode: parseInt(this.recordField.dataset.readOnly, 10) === 1,
......@@ -41,16 +33,25 @@ class SelectTreeElement {
validation: JSON.parse(this.recordField.dataset.formengineValidationRules)[0],
expandUpToLevel: this.recordField.dataset.treeExpandUpToLevel,
};
const initialized = tree.initialize(this.treeWrapper, settings);
if (!initialized) {
return;
}
tree.initialize(this.treeWrapper, settings);
tree.dispatch.on('nodeSelectedAfter.requestUpdate', () => { callback(); } );
this.listenForVisibleTree();
tree.dispatch.on('nodeSelectedAfter.requestUpdate', this.callback);
new TreeToolbar(this.treeWrapper);
}
if (this.recordField.dataset.treeShowToolbar) {
const selectTreeToolbar = new TreeToolbar();
selectTreeToolbar.initialize(this.treeWrapper);
/**
* If the Select item is in an invisible tab, it needs to be rendered once the tab
* becomes visible.
*/
private listenForVisibleTree(): void {
if (!this.treeWrapper.offsetParent) {
// Search for the parents that are tab containers
let idOfTabContainer = this.treeWrapper.closest('.tab-pane').getAttribute('id');
if (idOfTabContainer) {
let btn = document.querySelector('[aria-controls="' + idOfTabContainer + '"]');
btn.addEventListener('shown.bs.tab', () => { this.treeWrapper.dispatchEvent(new Event('svg-tree:visible')); });
}
}
}
......@@ -72,5 +73,3 @@ class SelectTreeElement {
return TYPO3.settings.ajaxUrls.record_tree_data + '&' + new URLSearchParams(params);
}
}
export = SelectTreeElement;
......@@ -29,7 +29,7 @@ export class TreeToolbar
toggleHideUnchecked: 'hide-unchecked-btn'
};
private treeContainer: HTMLElement;
private readonly treeContainer: HTMLElement;
private tree: any;
/**
......@@ -39,13 +39,9 @@ export class TreeToolbar
*/
private hideUncheckedState: boolean = false;
public constructor(settings: any = {}) {
Object.assign(this.settings, settings);
}
public initialize(treeContainer: HTMLElement): void {
public constructor(treeContainer: HTMLElement, settings: any = {}) {
this.treeContainer = treeContainer;
Object.assign(this.settings, settings);
if (!this.treeContainer.dataset.svgTreeInitialized
|| typeof (this.treeContainer as any).svgtree !== 'object'
) {
......@@ -53,11 +49,9 @@ export class TreeToolbar
//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.bind(this));
return;
} else {
this.render();
}
this.render();
}
/**
......
......@@ -14,23 +14,7 @@
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import ClientStorage = require('./Storage/Client');
enum Sizes {
small = 'small',
default = 'default',
large = 'large',
overlay = 'overlay',
}
enum States {
default = 'default',
disabled = 'disabled',
}
enum MarkupIdentifiers {
default = 'default',
inline = 'inline',
}
import {Sizes, States, MarkupIdentifiers} from './Enum/IconTypes';
interface PromiseCache {
[key: string]: Promise<string>;
......
......@@ -56,13 +56,13 @@ export class PageTreeDragDrop {
* Open node with children while holding the node/element over this node for 1 second
*/
public openNodeTimeout(): void {
if (this.tree.settings.nodeOver.node && this.tree.settings.nodeOver.node.hasChildren && !this.tree.settings.nodeOver.node.expanded) {
if (this.timeout.node != this.tree.settings.nodeOver.node) {
this.timeout.node = this.tree.settings.nodeOver;
if (this.tree.hoveredNode !== null && this.tree.hoveredNode.hasChildren && !this.tree.hoveredNode.expanded) {
if (this.timeout.node != this.tree.hoveredNode) {
this.timeout.node = this.tree.hoveredNode;
clearTimeout(this.timeout.time);
this.timeout.time = setTimeout(() => {
if (this.tree.settings.nodeOver.node) {
this.tree.showChildren(this.tree.settings.nodeOver.node);
if (this.tree.hoveredNode) {
this.tree.showChildren(this.tree.hoveredNode);
this.tree.prepareDataForVisibleNodes();
this.tree.update();
}
......@@ -95,12 +95,12 @@ export class PageTreeDragDrop {
if (y < 3) {
nodeBgBorder
.attr('transform', 'translate(-8, ' + (this.tree.settings.nodeOver.node.y - 10) + ')')
.attr('transform', 'translate(-8, ' + (this.tree.hoveredNode.y - 10) + ')')
.style('display', 'block');
if (this.tree.settings.nodeOver.node.depth === 0) {
if (this.tree.hoveredNode.depth === 0) {
this.addNodeDdClass($nodesWrap, $nodeDd, 'nodrop');
} else if (this.tree.settings.nodeOver.node.firstChild) {
} else if (this.tree.hoveredNode.firstChild) {
this.addNodeDdClass($nodesWrap, $nodeDd, 'ok-above');
} else {
this.addNodeDdClass($nodesWrap, $nodeDd, 'ok-between');
......@@ -110,17 +110,16 @@ export class PageTreeDragDrop {
} else if (y > 17) {
nodeBgBorder.style('display', 'none');
if (this.tree.settings.nodeOver.node.expanded && this.tree.settings.nodeOver.node.hasChildren) {
if (this.tree.hoveredNode.expanded && this.tree.hoveredNode.hasChildren) {
this.addNodeDdClass($nodesWrap, $nodeDd, 'ok-append');
this.tree.settings.nodeDragPosition = 'in';
} else {
nodeBgBorder
.attr('transform', 'translate(-8, ' + (this.tree.settings.nodeOver.node.y + 10) + ')')
.attr('transform', 'translate(-8, ' + (this.tree.hoveredNode.y + 10) + ')')
.style('display', 'block');
if (this.tree.settings.nodeOver.node.lastChild) {
if (this.tree.hoveredNode.lastChild) {
this.addNodeDdClass($nodesWrap, $nodeDd, 'ok-below');
} else {
this.addNodeDdClass($nodesWrap, $nodeDd, 'ok-between');
}
......
......@@ -138,7 +138,7 @@ export class ToolbarDragHandler implements DragDropHandler {
this.tree.nodesBgContainer.selectAll('.node-bg__border').style('display', 'none');
if (this.tree.settings.isDragAnDrop !== true || !this.tree.settings.nodeOver.node || !this.tree.isOverSvg) {
if (this.tree.settings.isDragAnDrop !== true || !this.tree.hoveredNode || !this.tree.isOverSvg) {
return false;
}
......@@ -149,7 +149,7 @@ export class ToolbarDragHandler implements DragDropHandler {
tooltip: this.tooltip,
icon: this.icon,
position: this.tree.settings.nodeDragPosition,
target: this.tree.settings.nodeOver.node
target: this.tree.hoveredNode
});
}
return true;
......@@ -173,6 +173,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
private isDragged: boolean = false;
private tree: any;
private dragDrop: any;
private nodeIsOverDelete: boolean = false;
constructor(tree: any, dragDrop: any) {
this.tree = tree;
......@@ -192,17 +193,17 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
.append('g')
.attr('class', 'nodes-drop-zone')
.attr('height', this.tree.settings.nodeHeight);
this.tree.nodeIsOverDelete = false;
this.nodeIsOverDelete = false;
this.dropZoneDelete.append('rect')
.attr('height', this.tree.settings.nodeHeight)
.attr('width', '50px')
.attr('x', 0)
.attr('y', 0)
.on('mouseover', () => {
this.tree.nodeIsOverDelete = true;
this.nodeIsOverDelete = true;
})
.on('mouseout', () => {
this.tree.nodeIsOverDelete = false;
this.nodeIsOverDelete = false;
});
this.dropZoneDelete.append('text')
......@@ -270,7 +271,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
});
if (node.isOver
|| (this.tree.settings.nodeOver.node && this.tree.settings.nodeOver.node.parentsStateIdentifier.indexOf(node.stateIdentifier) !== -1)
|| (this.tree.hoveredNode && this.tree.hoveredNode.parentsStateIdentifier.indexOf(node.stateIdentifier) !== -1)
|| !this.tree.isOverSvg) {
this.dragDrop.addNodeDdClass($nodesWrap, $nodeDd, 'nodrop');
......@@ -284,7 +285,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
if (this.dropZoneDelete && this.dropZoneDelete.node().dataset.open !== 'true' && this.tree.isOverSvg) {
this.animateDropZone('show', this.dropZoneDelete.node(), node);
}
} else if (!this.tree.settings.nodeOver.node) {
} else if (!this.tree.hoveredNode) {
this.dragDrop.addNodeDdClass($nodesWrap, $nodeDd, 'nodrop');
this.tree.nodesBgContainer
.selectAll('.node-bg__border')
......@@ -316,7 +317,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
let $svg = $(event.sourceEvent.target).closest('svg');
let $nodesBg = $svg.find('.nodes-bg');
let droppedNode = this.tree.settings.nodeOver.node;
let droppedNode = this.tree.hoveredNode;
this.isDragged = false;
this.dragDrop.addNodeDdClass($svg.find('.nodes-wrapper'), null, '', true);
......@@ -327,7 +328,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
if (
!(node.isOver
|| (this.tree.settings.nodeOver.node && this.tree.settings.nodeOver.node.parentsStateIdentifier.indexOf(node.stateIdentifier) !== -1)
|| (droppedNode && droppedNode.parentsStateIdentifier.indexOf(node.stateIdentifier) !== -1)
|| !this.tree.settings.canNodeDrag
|| !this.tree.isOverSvg
)
......@@ -368,7 +369,7 @@ export class PageTreeNodeDragHandler implements DragDropHandler {
}
Modal.dismiss();
});
} else if (this.tree.nodeIsOverDelete) {
} else if (this.nodeIsOverDelete) {
let options = this.dragDrop.changeNodePosition(droppedNode, 'delete');
if (this.tree.settings.displayDeleteConfirmation) {
let $modal = Modal.confirm(
......
......@@ -14,7 +14,7 @@
import {render} from 'lit-html';
import {html, TemplateResult} from 'lit-element';
import {icon} from 'TYPO3/CMS/Core/lit-helper';
import PageTree = require('TYPO3/CMS/Backend/PageTree/PageTree');
import {PageTree} from './PageTree';
import {PageTreeDragDrop} from './PageTreeDragDrop';
import viewPort from '../Viewport';
import {PageTreeToolbar} from './PageTreeToolbar';
......@@ -50,7 +50,7 @@ export class PageTreeElement {
filterUrl: filterUrl,
showIcons: true
});