Commit a6bfd190 authored by Benni Mack's avatar Benni Mack
Browse files

[TASK] Migrate TreeToolbar to TypeScript

This change migrate the TreeToolbar (used in FormEngine
Category Tree for example) to TypeScript,
removing effectively one RequireJS call and replacing
it with a native TypeScript component.

Removing the rest of the jQuery will be part of a
separate effort.

Resolves: #93432
Releases: master
Change-Id: I3df66cd1c28d096c0f32982347dbd37c58834e4d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67640

Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent d7cdf916
......@@ -12,6 +12,7 @@
*/
import SelectTree = require('TYPO3/CMS/Backend/FormEngine/Element/SelectTree');
import {TreeToolbar} from './TreeToolbar';
class SelectTreeElement {
private readonly treeWrapper: HTMLElement = null;
......@@ -48,10 +49,8 @@ class SelectTreeElement {
tree.dispatch.on('nodeSelectedAfter.requestUpdate', this.callback);
if (this.recordField.dataset.treeShowToolbar) {
require(['TYPO3/CMS/Backend/FormEngine/Element/TreeToolbar'], (TreeToolbar: any): void => {
const selectTreeToolbar = new TreeToolbar();
selectTreeToolbar.initialize(this.treeWrapper);
});
const selectTreeToolbar = new TreeToolbar();
selectTreeToolbar.initialize(this.treeWrapper);
}
}
......
/*
* 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 $ from 'jquery';
import {Tooltip} from 'bootstrap';
import {render} from 'lit-html';
import {html, TemplateResult} from 'lit-element';
import {icon, lll} from 'TYPO3/CMS/Core/lit-helper';
/**
* @exports TYPO3/CMS/Backend/FormEngine/Element/TreeToolbar
*/
export class TreeToolbar
{
private settings = {
toolbarSelector: 'tree-toolbar btn-toolbar',
collapseAllBtn: 'collapse-all-btn',
expandAllBtn: 'expand-all-btn',
searchInput: 'search-input',
toggleHideUnchecked: 'hide-unchecked-btn'
};
private $treeWrapper: JQuery;
private treeContainer: HTMLElement;
private tree: any;
/**
* State of the hide unchecked toggle button
*
* @type {boolean}
*/
private hideUncheckedState: boolean = false;
public constructor(settings: any = {}) {
Object.assign(this.settings, settings);
}
public initialize(treeContainer: HTMLElement): void {
this.treeContainer = treeContainer;
this.$treeWrapper = $(treeContainer);
if (!this.$treeWrapper.data('svgtree-initialized')
|| typeof this.$treeWrapper.data('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.$treeWrapper.on('svgTree.initialized', () => this.render());
return;
}
this.render();
}
/**
* Collapse children of root node
*/
private collapseAll() {
this.tree.collapseAll();
};
/**
* Expand all nodes
*/
private expandAll() {
this.tree.expandAll();
};
private search(event: InputEvent): void {
const inputEl = <HTMLInputElement>event.target;
if (this.tree.nodes.length) {
this.tree.nodes[0].open = false;
}
const name = inputEl.value.trim()
const regex = new RegExp(name, 'i');
this.tree.nodes.forEach((node: any) => {
if (regex.test(node.name)) {
this.showParents(node);
node.open = true;
node.hidden = false;
} else {
node.hidden = true;
node.open = false;
}
});
this.tree.prepareDataForVisibleNodes();
this.tree.update();
}
/**
* Show only checked items
*/
private toggleHideUnchecked(): void {
this.hideUncheckedState = !this.hideUncheckedState;
if (this.hideUncheckedState) {
this.tree.nodes.forEach((node: any) => {
if (node.checked) {
this.showParents(node);
node.expanded = true;
node.hidden = false;
} else {
node.hidden = true;
node.expanded = false;
}
});
} else {
this.tree.nodes.forEach((node: any) => node.hidden = false);
}
this.tree.prepareDataForVisibleNodes();
this.tree.update();
}
/**
* Finds and show all parents of node
*/
private showParents(node: any): void {
if (node.parents.length === 0) {
return;
}
const parent = this.tree.nodes[node.parents[0]];
parent.hidden = false;
// expand parent node
parent.expanded = true;
this.showParents(parent);
}
private render(): void {
this.tree = this.$treeWrapper.data('svgtree');
// @todo Better use initialize() settings, drop this assignment here
Object.assign(this.settings, this.tree.settings);
const placeholderElement = document.createElement('div');
this.treeContainer.prepend(placeholderElement);
render(this.renderTemplate(), placeholderElement);
const toolbarElement = this.treeContainer
.querySelector('.' + this.settings.toolbarSelector);
if (toolbarElement) {
toolbarElement.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((tooltipTriggerEl: HTMLElement) => new Tooltip(tooltipTriggerEl));
}
}
private renderTemplate(): TemplateResult {
return html`
<div class="${this.settings.toolbarSelector}">
<div class="input-group">
<span class="input-group-addon input-group-icon filter">${icon('actions-filter', 'small')}</span>
<input type="text" class="form-control ${this.settings.searchInput}" placeholder="${lll('tcatree.findItem')}" @input="${(evt: InputEvent) => this.search(evt)}">
</div>
<div class="btn-group">
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.expandAllBtn}" title="${lll('tcatree.expandAll')}" @click="${() => this.expandAll()}">
${icon('apps-pagetree-category-expand-all', 'small')}
</button>
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.collapseAllBtn}" title="${lll('tcatree.collapseAll')}" @click="${() => this.collapseAll()}">
${icon('apps-pagetree-category-collapse-all', 'small')}
</button>
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.toggleHideUnchecked}" title="${lll('tcatree.toggleHideUnchecked')}" @click="${() => this.toggleHideUnchecked()}">
${icon('apps-pagetree-category-toggle-hide-checked', 'small')}
</button>
</div>
</div>
`;
}
}
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","TYPO3/CMS/Backend/FormEngine/Element/SelectTree"],(function(e,t,r){"use strict";return class{constructor(e,t,r){this.treeWrapper=null,this.recordField=null,this.callback=null,this.treeWrapper=document.getElementById(e),this.recordField=document.getElementById(t),this.callback=r,this.initialize()}initialize(){const t=this.generateRequestUrl(),i=new r,a={dataUrl:t,showIcons:!0,showCheckboxes:!0,readOnlyMode:1===parseInt(this.recordField.dataset.readOnly,10),input:this.recordField,exclusiveNodesIdentifiers:this.recordField.dataset.treeExclusiveKeys,validation:JSON.parse(this.recordField.dataset.formengineValidationRules)[0],expandUpToLevel:this.recordField.dataset.treeExpandUpToLevel};i.initialize(this.treeWrapper,a)&&(i.dispatch.on("nodeSelectedAfter.requestUpdate",this.callback),this.recordField.dataset.treeShowToolbar&&e(["TYPO3/CMS/Backend/FormEngine/Element/TreeToolbar"],e=>{(new e).initialize(this.treeWrapper)}))}generateRequestUrl(){const e={tableName:this.recordField.dataset.tablename,fieldName:this.recordField.dataset.fieldname,uid:this.recordField.dataset.uid,recordTypeValue:this.recordField.dataset.recordtypevalue,dataStructureIdentifier:this.recordField.dataset.datastructureidentifier,flexFormSheetName:this.recordField.dataset.flexformsheetname,flexFormFieldName:this.recordField.dataset.flexformfieldname,flexFormContainerName:this.recordField.dataset.flexformcontainername,flexFormContainerIdentifier:this.recordField.dataset.flexformcontaineridentifier,flexFormContainerFieldName:this.recordField.dataset.flexformcontainerfieldname,flexFormSectionContainerIsNew:this.recordField.dataset.flexformsectioncontainerisnew,command:this.recordField.dataset.command};return TYPO3.settings.ajaxUrls.record_tree_data+"&"+new URLSearchParams(e)}}}));
\ No newline at end of file
define(["require","exports","TYPO3/CMS/Backend/FormEngine/Element/SelectTree","./TreeToolbar"],(function(e,t,r,i){"use strict";return class{constructor(e,t,r){this.treeWrapper=null,this.recordField=null,this.callback=null,this.treeWrapper=document.getElementById(e),this.recordField=document.getElementById(t),this.callback=r,this.initialize()}initialize(){const e=this.generateRequestUrl(),t=new r,a={dataUrl:e,showIcons:!0,showCheckboxes:!0,readOnlyMode:1===parseInt(this.recordField.dataset.readOnly,10),input:this.recordField,exclusiveNodesIdentifiers:this.recordField.dataset.treeExclusiveKeys,validation:JSON.parse(this.recordField.dataset.formengineValidationRules)[0],expandUpToLevel:this.recordField.dataset.treeExpandUpToLevel};if(t.initialize(this.treeWrapper,a)&&(t.dispatch.on("nodeSelectedAfter.requestUpdate",this.callback),this.recordField.dataset.treeShowToolbar)){(new i.TreeToolbar).initialize(this.treeWrapper)}}generateRequestUrl(){const e={tableName:this.recordField.dataset.tablename,fieldName:this.recordField.dataset.fieldname,uid:this.recordField.dataset.uid,recordTypeValue:this.recordField.dataset.recordtypevalue,dataStructureIdentifier:this.recordField.dataset.datastructureidentifier,flexFormSheetName:this.recordField.dataset.flexformsheetname,flexFormFieldName:this.recordField.dataset.flexformfieldname,flexFormContainerName:this.recordField.dataset.flexformcontainername,flexFormContainerIdentifier:this.recordField.dataset.flexformcontaineridentifier,flexFormContainerFieldName:this.recordField.dataset.flexformcontainerfieldname,flexFormSectionContainerIsNew:this.recordField.dataset.flexformsectioncontainerisnew,command:this.recordField.dataset.command};return TYPO3.settings.ajaxUrls.record_tree_data+"&"+new URLSearchParams(e)}}}));
\ No newline at end of file
......@@ -10,217 +10,22 @@
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Module: TYPO3/CMS/Backend/FormEngine/Element/TreeToolbar
*/
define(['jquery',
'TYPO3/CMS/Backend/Icons',
'TYPO3/CMS/Backend/Tooltip',
'TYPO3/CMS/Backend/SvgTree'
],
function($, Icons) {
'use strict';
/**
* TreeToolbar class
*
* @constructor
* @exports TYPO3/CMS/Backend/FormEngine/Element/TreeToolbar
*/
var TreeToolbar = function() {
this.settings = {
toolbarSelector: '.tree-toolbar',
collapseAllBtn: '.collapse-all-btn',
expandAllBtn: '.expand-all-btn',
searchInput: '.search-input',
toggleHideUnchecked: '.hide-unchecked-btn'
};
/**
* jQuery object wrapping the SvgTree
*
* @type {jQuery}
*/
this.$treeWrapper = null;
/**
* SvgTree instance
*
* @type {SvgTree}
*/
this.tree = null;
/**
* State of the hide unchecked toggle button
*
* @type {boolean}
* @private
*/
this._hideUncheckedState = false;
/**
* Toolbar template
*
* @type {jQuery}
*/
this.$template = $(
'<div class="tree-toolbar btn-toolbar">' +
'<div class="input-group">' +
'<span class="input-group-addon input-group-icon filter"></span>' +
'<input type="text" class="form-control search-input" placeholder="' + TYPO3.lang['tcatree.findItem'] + '">' +
'</div>' +
'<div class="btn-group">' +
'<button type="button" data-bs-toggle="tooltip" class="btn btn-default expand-all-btn" title="' + TYPO3.lang['tcatree.expandAll'] + '"></button>' +
'<button type="button" data-bs-toggle="tooltip" class="btn btn-default collapse-all-btn" title="' + TYPO3.lang['tcatree.collapseAll'] + '"></button>' +
'<button type="button" data-bs-toggle="tooltip" class="btn btn-default hide-unchecked-btn" title="' + TYPO3.lang['tcatree.toggleHideUnchecked'] + '"></button>' +
'</div>' +
'</div>'
);
};
/**
* Toolbar initialization
*
* @param {String} treeSelector
* @param {Object} settings
*/
TreeToolbar.prototype.initialize = function(treeSelector, settings) {
this.$treeWrapper = $(treeSelector);
if (!this.$treeWrapper.data('svgtree-initialized') || typeof this.$treeWrapper.data('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.$treeWrapper.on('svgTree.initialized', this.render.bind(this));
return;
}
$.extend(this.settings, settings);
this.render();
};
/**
* Renders toolbar
*/
TreeToolbar.prototype.render = function() {
var _this = this;
this.tree = this.$treeWrapper.data('svgtree');
var $toolbar = this.$template.clone().insertBefore(this.$treeWrapper);
Icons.getIcon('actions-filter', Icons.sizes.small).then(function(icon) {
$toolbar.find('.filter').append(icon);
});
Icons.getIcon('apps-pagetree-category-expand-all', Icons.sizes.small).then(function(icon) {
$toolbar.find('.expand-all-btn').append(icon);
});
Icons.getIcon('apps-pagetree-category-collapse-all', Icons.sizes.small).then(function(icon) {
$toolbar.find('.collapse-all-btn').append(icon);
});
Icons.getIcon('apps-pagetree-category-toggle-hide-checked', Icons.sizes.small).then(function(icon) {
$toolbar.find('.hide-unchecked-btn').append(icon);
});
$toolbar.find(this.settings.collapseAllBtn).on('click', this.collapseAll.bind(this));
$toolbar.find(this.settings.expandAllBtn).on('click', this.expandAll.bind(this));
$toolbar.find(this.settings.searchInput).on('input', function() {
_this.search.call(_this, this);
});
$toolbar.find(this.settings.toggleHideUnchecked).on('click', this.toggleHideUnchecked.bind(this));
$toolbar.find('[data-bs-toggle="tooltip"]').tooltip();
};
/**
* Collapse children of root node
*/
TreeToolbar.prototype.collapseAll = function() {
this.tree.collapseAll();
};
/**
* Expand all nodes
*/
TreeToolbar.prototype.expandAll = function() {
this.tree.expandAll();
};
/**
* Find node by name
*
* @param {HTMLElement} input
*/
TreeToolbar.prototype.search = function(input) {
var _this = this;
var name = $(input).val();
this.tree.nodes[0].open = false;
this.tree.nodes.forEach(function(node) {
var regex = new RegExp(name, 'i');
if (regex.test(node.name)) {
_this.showParents(node);
node.open = true;
node.hidden = false;
} else {
node.hidden = true;
node.open = false;
}
});
this.tree.prepareDataForVisibleNodes();
this.tree.update();
};
/**
* Show only checked items
*/
TreeToolbar.prototype.toggleHideUnchecked = function() {
var _this = this;
this._hideUncheckedState = !this._hideUncheckedState;
if (this._hideUncheckedState) {
this.tree.nodes.forEach(function(node) {
if (node.checked) {
_this.showParents(node);
node.open = true;
node.hidden = false;
} else {
node.hidden = true;
node.open = false;
}
});
} else {
this.tree.nodes.forEach(function(node) {
node.hidden = false;
});
}
this.tree.prepareDataForVisibleNodes();
this.tree.update();
};
/**
* Finds and show all parents of node
*
* @param {Node} node
* @returns {Boolean}
*/
TreeToolbar.prototype.showParents = function(node) {
if (node.parents.length === 0) {
return true;
}
var parent = this.tree.nodes[node.parents[0]];
parent.hidden = false;
//expand parent node
parent.open = true;
this.showParents(parent);
};
return TreeToolbar;
});
var __importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","jquery","bootstrap","lit-html","lit-element","TYPO3/CMS/Core/lit-helper"],(function(e,t,s,l,i,n,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.TreeToolbar=void 0,s=__importDefault(s);t.TreeToolbar=class{constructor(e={}){this.settings={toolbarSelector:"tree-toolbar btn-toolbar",collapseAllBtn:"collapse-all-btn",expandAllBtn:"expand-all-btn",searchInput:"search-input",toggleHideUnchecked:"hide-unchecked-btn"},this.hideUncheckedState=!1,Object.assign(this.settings,e)}initialize(e){this.treeContainer=e,this.$treeWrapper=s.default(e),this.$treeWrapper.data("svgtree-initialized")&&"object"==typeof this.$treeWrapper.data("svgtree")?this.render():this.$treeWrapper.on("svgTree.initialized",()=>this.render())}collapseAll(){this.tree.collapseAll()}expandAll(){this.tree.expandAll()}search(e){const t=e.target;this.tree.nodes.length&&(this.tree.nodes[0].open=!1);const s=t.value.trim(),l=new RegExp(s,"i");this.tree.nodes.forEach(e=>{l.test(e.name)?(this.showParents(e),e.open=!0,e.hidden=!1):(e.hidden=!0,e.open=!1)}),this.tree.prepareDataForVisibleNodes(),this.tree.update()}toggleHideUnchecked(){this.hideUncheckedState=!this.hideUncheckedState,this.hideUncheckedState?this.tree.nodes.forEach(e=>{e.checked?(this.showParents(e),e.expanded=!0,e.hidden=!1):(e.hidden=!0,e.expanded=!1)}):this.tree.nodes.forEach(e=>e.hidden=!1),this.tree.prepareDataForVisibleNodes(),this.tree.update()}showParents(e){if(0===e.parents.length)return;const t=this.tree.nodes[e.parents[0]];t.hidden=!1,t.expanded=!0,this.showParents(t)}render(){this.tree=this.$treeWrapper.data("svgtree"),Object.assign(this.settings,this.tree.settings);const e=document.createElement("div");this.treeContainer.prepend(e),i.render(this.renderTemplate(),e);const t=this.treeContainer.querySelector("."+this.settings.toolbarSelector);t&&t.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(e=>new l.Tooltip(e))}renderTemplate(){return n.html`
<div class="${this.settings.toolbarSelector}">
<div class="input-group">
<span class="input-group-addon input-group-icon filter">${r.icon("actions-filter","small")}</span>
<input type="text" class="form-control ${this.settings.searchInput}" placeholder="${r.lll("tcatree.findItem")}" @input="${e=>this.search(e)}">
</div>
<div class="btn-group">
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.expandAllBtn}" title="${r.lll("tcatree.expandAll")}" @click="${()=>this.expandAll()}">
${r.icon("apps-pagetree-category-expand-all","small")}
</button>
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.collapseAllBtn}" title="${r.lll("tcatree.collapseAll")}" @click="${()=>this.collapseAll()}">
${r.icon("apps-pagetree-category-collapse-all","small")}
</button>
<button type="button" data-bs-toggle="tooltip" class="btn btn-default ${this.settings.toggleHideUnchecked}" title="${r.lll("tcatree.toggleHideUnchecked")}" @click="${()=>this.toggleHideUnchecked()}">
${r.icon("apps-pagetree-category-toggle-hide-checked","small")}
</button>
</div>
</div>
`}}}));
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment