Commit 428a9942 authored by Benni Mack's avatar Benni Mack Committed by Christian Kuhn
Browse files

[TASK] Decouple PageTree from NavigationComponent

In order to use the non-iframe navigation component properly,
the current ties to "PageTree" (which was de-facto the only possible
navigation component to be used until TYPO3 v11 due to heavy
coupling) are removed.

The TreeInterface, which served as a wrapper for the PageTree
Navigation Component is removed, and replaced by the NavigationComponent
interface in TypeScript.

In addition, loading and initializing (and keeping state) of the
used navigation component now solely relies in the NavigationContainer.

This in turn, finally allows to e.g. properly re-load the FileStorageTree
and PageTree separately (see BackendUtility change) while
incorporating some ES6 nullable functionality.

In addition, the NavigationContainer is now jQuery-free.

Resolves: #93672
Releases: master
Change-Id: Ia16a3e789459225b21fa12efd1db6f3b4500cfef
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/68201


Tested-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Reviewed-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 55711b5b
......@@ -47,8 +47,8 @@ class AjaxDataHandler {
* Refresh the page tree
*/
private static refreshPageTree(): void {
if (Viewport.NavigationContainer && Viewport.NavigationContainer.PageTree) {
Viewport.NavigationContainer.PageTree.refreshTree();
if (Viewport.NavigationContainer) {
Viewport.NavigationContainer.getComponentByName('PageTree')?.refresh();
}
}
......
......@@ -21,6 +21,7 @@ import Modal = require('./Modal');
import ModuleMenu = require('./ModuleMenu');
import Notification = require('TYPO3/CMS/Backend/Notification');
import Viewport = require('./Viewport');
import { PageTree } from './PageTree/PageTree';
/**
* @exports TYPO3/CMS/Backend/ContextMenuActions
......@@ -76,7 +77,7 @@ class ContextMenuActions {
*/
public static mountAsTreeRoot(table: string, uid: number): void {
if (table === 'pages') {
Viewport.NavigationContainer.PageTree.setTemporaryMountPoint(uid);
Viewport.NavigationContainer.getComponentByName('PageTree')?.apply((component: PageTree) => { component.setTemporaryMountPoint(uid); });
}
}
......@@ -159,7 +160,7 @@ class ContextMenuActions {
+ '&data[' + table + '][' + uid + '][' + disableFieldName + ']=1'
+ '&redirect=' + ContextMenuActions.getReturnUrl(),
).done((): void => {
Viewport.NavigationContainer.PageTree.refreshTree();
Viewport.NavigationContainer.getComponentByName('PageTree')?.refresh();
});
}
......@@ -174,7 +175,7 @@ class ContextMenuActions {
+ '&data[' + table + '][' + uid + '][' + disableFieldName + ']=0'
+ '&redirect=' + ContextMenuActions.getReturnUrl(),
).done((): void => {
Viewport.NavigationContainer.PageTree.refreshTree();
Viewport.NavigationContainer.getComponentByName('PageTree')?.refresh();
});
}
......@@ -188,7 +189,7 @@ class ContextMenuActions {
+ '&data[' + table + '][' + uid + '][nav_hide]=0'
+ '&redirect=' + ContextMenuActions.getReturnUrl(),
).done((): void => {
Viewport.NavigationContainer.PageTree.refreshTree();
Viewport.NavigationContainer.getComponentByName('PageTree')?.refresh();
});
}
......@@ -202,7 +203,7 @@ class ContextMenuActions {
+ '&data[' + table + '][' + uid + '][nav_hide]=1'
+ '&redirect=' + ContextMenuActions.getReturnUrl(),
).done((): void => {
Viewport.NavigationContainer.PageTree.refreshTree();
Viewport.NavigationContainer.getComponentByName('PageTree')?.refresh();
});
}
......@@ -233,13 +234,14 @@ class ContextMenuActions {
if (e.target.getAttribute('name') === 'delete') {
const eventData = {component: 'contextmenu', action: 'delete', table, uid};
AjaxDataHandler.process('cmd[' + table + '][' + uid + '][delete]=1', eventData).then((): void => {
if (table === 'pages' && Viewport.NavigationContainer.PageTree) {
if (table === 'pages' && Viewport.NavigationContainer) {
if (uid === top.fsMod.recentIds.web) {
let node = Viewport.NavigationContainer.PageTree.getFirstNode();
Viewport.NavigationContainer.PageTree.selectNode(node);
Viewport.NavigationContainer.getComponentByName('PageTree')?.apply((pageTree: PageTree) => {
let node = pageTree.getFirstNode();
pageTree.selectNode(node);
});
}
Viewport.NavigationContainer.PageTree.refreshTree();
Viewport.NavigationContainer.getComponentByName('PageTree')?.refresh();
}
});
}
......@@ -347,8 +349,8 @@ class ContextMenuActions {
Viewport.ContentContainer.setUrl(
top.TYPO3.settings.RecordCommit.moduleUrl + url,
).done((): void => {
if (table === 'pages' && Viewport.NavigationContainer.PageTree) {
Viewport.NavigationContainer.PageTree.refreshTree();
if (table === 'pages') {
Viewport.NavigationContainer.getComponentByName('PageTree')?.refresh();
}
});
};
......
......@@ -21,7 +21,6 @@ import TriggerRequest = require('./Event/TriggerRequest');
import InteractionRequest = require('./Event/InteractionRequest');
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
import {PageTreeElement} from './PageTree/PageTreeElement';
interface Module {
name: string;
......@@ -36,7 +35,6 @@ interface Module {
*/
class ModuleMenu {
private loadedModule: string = null;
private loadedNavigationComponentId: string = '';
private spaceKeyPressedOnCollapsible: boolean = false;
/**
......@@ -464,7 +462,7 @@ class ModuleMenu {
$.proxy(
(): void => {
if (moduleData.navigationComponentId) {
this.loadNavigationComponent(moduleData.navigationComponentId);
Viewport.NavigationContainer.showComponent(moduleData.navigationComponentId);
} else if (moduleData.navigationFrameScript) {
Viewport.NavigationContainer.show('typo3-navigationIframe');
this.openInNavFrame(
......@@ -502,39 +500,6 @@ class ModuleMenu {
return deferred;
}
/**
* Renders registered (non-iframe) navigation component e.g. a page tree
*
* @param {string} navigationComponentId
*/
private loadNavigationComponent(navigationComponentId: string): void {
const me = this;
Viewport.NavigationContainer.show(navigationComponentId);
if (navigationComponentId === this.loadedNavigationComponentId) {
return;
}
const componentCssName = navigationComponentId.replace(/[/]/g, '_');
if (this.loadedNavigationComponentId !== '') {
$('#navigationComponent-' + this.loadedNavigationComponentId.replace(/[/]/g, '_')).hide();
}
if ($('.t3js-scaffold-content-navigation [data-component="' + navigationComponentId + '"]').length < 1) {
$('.t3js-scaffold-content-navigation')
.append($('<div />', {
'class': 'scaffold-content-navigation-component',
'data-component': navigationComponentId,
id: 'navigationComponent-' + componentCssName,
}));
}
require([navigationComponentId], (__esModule: any): void => {
const NavigationComponent = Object.values(__esModule)[0] as typeof PageTreeElement;
NavigationComponent.initialize('#navigationComponent-' + componentCssName);
Viewport.NavigationContainer.show(navigationComponentId);
me.loadedNavigationComponentId = navigationComponentId;
});
}
/**
* @param {string} url
* @param {string} params
......
......@@ -210,7 +210,7 @@ class PageActions {
$inputFieldWrap.find('[data-action=cancel]').trigger('click');
this.$pageTitle.text($field.val());
this.initializePageTitleRenaming();
top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree();
top.TYPO3.Backend.NavigationContainer.getComponentByName('PageTree')?.refresh();
}).catch((): void => {
$inputFieldWrap.find('[data-action=cancel]').trigger('click');
});
......
......@@ -20,14 +20,13 @@ import Icons = require('../Icons');
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;
}
export class PageTree extends SvgTree implements TreeInterface
export class PageTree extends SvgTree
{
public settings: PageTreeSettings;
protected networkErrorTitle: string = TYPO3.lang.pagetree_networkErrorTitle;
......
......@@ -16,19 +16,39 @@ import {html, TemplateResult} from 'lit-element';
import {lll} from 'TYPO3/CMS/Core/lit-helper';
import {PageTree} from './PageTree';
import {PageTreeDragDrop, ToolbarDragHandler} from './PageTreeDragDrop';
import viewPort from '../Viewport';
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';
import 'TYPO3/CMS/Backend/Element/IconElement';
import {NavigationComponent} from 'TYPO3/CMS/Backend/Viewport/NavigationComponent';
/**
* @exports TYPO3/CMS/Backend/PageTree/PageTreeElement
*/
export class PageTreeElement {
public static initialize(selector: string): void {
export class PageTreeElement implements NavigationComponent {
private readonly tree: PageTree;
private static renderTemplate(): TemplateResult {
return html`
<div id="typo3-pagetree" class="svg-tree">
<div>
<div id="typo3-pagetree-toolbar" class="svg-toolbar"></div>
<div id="typo3-pagetree-treeContainer" class="navigation-tree-container">
<div id="typo3-pagetree-tree" class="svg-tree-wrapper">
<div class="node-loader">
<typo3-backend-icon identifier="spinner-circle-light" size="small"></typo3-backend-icon>
</div>
</div>
</div>
</div>
<div class="svg-tree-loader">
<typo3-backend-icon identifier="spinner-circle-light" size="large"></typo3-backend-icon>
</div>
</div>
`;
}
public constructor(selector: string) {
const targetEl = document.querySelector(selector);
// let SvgTree know it shall be visible
......@@ -40,8 +60,8 @@ export class PageTreeElement {
render(PageTreeElement.renderTemplate(), targetEl);
const treeEl = <HTMLElement>targetEl.querySelector('.svg-tree-wrapper');
const tree = new PageTree();
const dragDrop = new PageTreeDragDrop(tree);
this.tree = new PageTree();
const dragDrop = new PageTreeDragDrop(this.tree);
const configurationUrl = top.TYPO3.settings.ajaxUrls.page_tree_configuration;
(new AjaxRequest(configurationUrl)).get()
.then(async (response: AjaxResponse): Promise<void> => {
......@@ -53,8 +73,7 @@ export class PageTreeElement {
filterUrl: filterUrl,
showIcons: true
});
tree.initialize(treeEl, configuration, dragDrop);
viewPort.NavigationContainer.setComponentInstance(tree);
this.tree.initialize(treeEl, configuration, dragDrop);
// the toolbar relies on settings retrieved in this step
const toolbar = <HTMLElement>targetEl.querySelector('.svg-toolbar');
if (!toolbar.dataset.treeShowToolbar) {
......@@ -64,25 +83,17 @@ export class PageTreeElement {
}
});
}
private static renderTemplate(): TemplateResult {
return html`
<div id="typo3-pagetree" class="svg-tree">
<div>
<div id="typo3-pagetree-toolbar" class="svg-toolbar"></div>
<div id="typo3-pagetree-treeContainer" class="navigation-tree-container">
<div id="typo3-pagetree-tree" class="svg-tree-wrapper">
<div class="node-loader">
<typo3-backend-icon identifier="spinner-circle-light" size="small"></typo3-backend-icon>
</div>
</div>
</div>
</div>
<div class="svg-tree-loader">
<typo3-backend-icon identifier="spinner-circle-light" size="large"></typo3-backend-icon>
</div>
</div>
`;
public getName(): string {
return 'PageTree';
}
public refresh?(): void {
this.tree.refreshOrFilterTree();
}
public select(item: any): void {
this.tree.selectNode(item);
}
public apply(fn: Function): void {
fn(this.tree);
}
}
......@@ -203,3 +214,4 @@ class Toolbar {
return this.dragDrop.connectDragHandler(new ToolbarDragHandler(item, this.tree, this.dragDrop));
}
}
......@@ -18,9 +18,8 @@ import ContextMenu = require('../ContextMenu');
import Persistent from '../Storage/Persistent';
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import {FileStorageTreeNodeDragHandler, FileStorageTreeActions} from './FileStorageTreeActions';
import {TreeInterface} from '../Viewport/TreeInterface';
export class FileStorageTree extends SvgTree implements TreeInterface {
export class FileStorageTree extends SvgTree {
public settings: SvgTreeSettings;
public searchQuery: string = '';
protected networkErrorTitle: string = TYPO3.lang.tree_networkError;
......@@ -168,13 +167,6 @@ export class FileStorageTree extends SvgTree implements TreeInterface {
this.refreshTree();
}
}
public setTemporaryMountPoint(pid: number): void {
// stub
}
public unsetTemporaryMountPoint() {
// stub
}
/**
* Initializes a drag&drop when called on the tree. Should be moved somewhere else at some point
......
......@@ -15,16 +15,36 @@ import {render} from 'lit-html';
import {html, TemplateResult} from 'lit-element';
import {lll} from 'TYPO3/CMS/Core/lit-helper';
import {FileStorageTree} from './FileStorageTree';
import viewPort from '../Viewport';
import DebounceEvent from 'TYPO3/CMS/Core/Event/DebounceEvent';
import {FileStorageTreeActions} from './FileStorageTreeActions';
import 'TYPO3/CMS/Backend/Element/IconElement';
import {NavigationComponent} from 'TYPO3/CMS/Backend/Viewport/NavigationComponent';
/**
* Responsible for setting up the viewport for the Navigation Component for the File Tree
*/
export class FileStorageTreeContainer {
public static initialize(selector: string): void {
export class FileStorageTreeContainer implements NavigationComponent {
private readonly tree: FileStorageTree;
private static renderTemplate(): TemplateResult {
return html`
<div id="typo3-filestoragetree" class="svg-tree">
<div>
<div id="filestoragetree-toolbar" class="svg-toolbar"></div>
<div class="navigation-tree-container">
<div id="typo3-filestoragetree-tree" class="svg-tree-wrapper">
<div class="node-loader">
<typo3-backend-icon identifier="spinner-circle-light" size="small"></typo3-backend-icon>
</div>
</div>
</div>
</div>
<div class="svg-tree-loader">
<typo3-backend-icon identifier="spinner-circle-light" size="large"></typo3-backend-icon>
</div>
</div>
`;
}
public constructor(selector: string) {
const targetEl = document.querySelector(selector);
// let SvgTree know it shall be visible
......@@ -36,42 +56,33 @@ export class FileStorageTreeContainer {
render(FileStorageTreeContainer.renderTemplate(), targetEl);
const treeEl = <HTMLElement>targetEl.querySelector('.svg-tree-wrapper');
const tree = new FileStorageTree();
const actions = new FileStorageTreeActions(tree);
tree.initialize(treeEl, {
this.tree = new FileStorageTree();
const actions = new FileStorageTreeActions(this.tree);
this.tree.initialize(treeEl, {
dataUrl: top.TYPO3.settings.ajaxUrls.filestorage_tree_data,
filterUrl: top.TYPO3.settings.ajaxUrls.filestorage_tree_filter,
showIcons: true
}, actions);
viewPort.NavigationContainer.setComponentInstance(tree);
// Activate the toolbar
const toolbar = <HTMLElement>targetEl.querySelector('.svg-toolbar');
new Toolbar(treeEl, toolbar);
// event listener updating current tree state
document.addEventListener('typo3:filelist:treeUpdateRequested', (evt: CustomEvent) => {
tree.selectNodeByIdentifier(evt.detail.payload.identifier);
this.tree.selectNodeByIdentifier(evt.detail.payload.identifier);
});
}
private static renderTemplate(): TemplateResult {
return html`
<div id="typo3-filestoragetree" class="svg-tree">
<div>
<div id="filestoragetree-toolbar" class="svg-toolbar"></div>
<div class="navigation-tree-container">
<div id="typo3-filestoragetree-tree" class="svg-tree-wrapper">
<div class="node-loader">
<typo3-backend-icon identifier="spinner-circle-light" size="small"></typo3-backend-icon>
</div>
</div>
</div>
</div>
<div class="svg-tree-loader">
<typo3-backend-icon identifier="spinner-circle-light" size="large"></typo3-backend-icon>
</div>
</div>
`;
public getName(): string {
return 'FileStorageTree';
}
public refresh?(): void {
this.tree.refreshOrFilterTree();
}
public select(item: any): void {
this.tree.selectNode(item);
}
public apply(fn: Function): void {
fn(this.tree);
}
}
......
......@@ -11,11 +11,18 @@
* The TYPO3 project - inspiring people to share!
*/
export interface TreeInterface {
refreshTree?(): void;
refreshOrFilterTree?(): void;
setTemporaryMountPoint(pid: number): void;
unsetTemporaryMountPoint(): void;
selectNode(node: any): void;
getFirstNode(): any;
/**
* This interface defines the very minimum a navigation component needs to contain.
*
* It is mainly used in the NavigationContainer to load Components.
*/
export interface NavigationComponent {
// A unique identifier, e.g. "PageTree"
getName(): string;
// Used if the component should refresh itself
refresh?(): void;
// Used to select an item inside the component
select(item: any): void;
// Apply any callback, and allow any item to be handed into the callback function
apply(callback: Function): void;
}
......@@ -13,18 +13,16 @@
import {ScaffoldIdentifierEnum} from '../Enum/Viewport/ScaffoldIdentifier';
import {AbstractContainer} from './AbstractContainer';
import $ from 'jquery';
import PageTree = require('./PageTree');
import TriggerRequest = require('../Event/TriggerRequest');
import InteractionRequest = require('../Event/InteractionRequest');
import {TreeInterface} from 'TYPO3/CMS/Backend/Viewport/TreeInterface';
import {NavigationComponent} from 'TYPO3/CMS/Backend/Viewport/NavigationComponent';
class NavigationContainer extends AbstractContainer {
public PageTree: PageTree = null;
private instance: TreeInterface = null;
private components: Array<NavigationComponent> = [];
private readonly parent: HTMLElement;
private readonly container: HTMLElement;
private readonly switcher: HTMLElement = null;
private activeComponentId: string = '';
public constructor(consumerScope: any, navigationSwitcher?: HTMLElement)
{
......@@ -33,15 +31,52 @@ class NavigationContainer extends AbstractContainer {
this.container = document.querySelector(ScaffoldIdentifierEnum.contentNavigation);
this.switcher = navigationSwitcher;
}
/**
* Public method used by Navigation components to register themselves.
* See TYPO3/CMS/Backend/PageTree/PageTreeElement->initialize
* Renders registered (non-iframe) navigation component e.g. a page tree
*
* @param {TreeInterface} component
* @param {string} navigationComponentId
*/
public setComponentInstance(component: TreeInterface): void {
this.instance = component;
this.PageTree = new PageTree(component);
public showComponent(navigationComponentId: string): void {
this.show(navigationComponentId);
// Component is already loaded and active, nothing to do
if (navigationComponentId === this.activeComponentId) {
return;
}
if (this.activeComponentId !== '') {
let activeComponentElement = this.container.querySelector('#navigationComponent-' + this.activeComponentId.replace(/[/]/g, '_')) as HTMLElement;
if (activeComponentElement) {
activeComponentElement.style.display = 'none';
}
}
const componentCssName = navigationComponentId.replace(/[/]/g, '_');
const navigationComponentElement = 'navigationComponent-' + componentCssName;
// Component does not exist, create the div as wrapper
if (this.container.querySelectorAll('[data-component="' + navigationComponentId + '"]').length === 0) {
this.container.insertAdjacentHTML(
'beforeend',
'<div class="scaffold-content-navigation-component" data-component="' + navigationComponentId + '" id="' + navigationComponentElement + '"></div>'
);
}
require([navigationComponentId], (__esModule: any): void => {
// @ts-ignore
const navigationComponent = (new (Object.values(__esModule)[0])('#' + navigationComponentElement)) as NavigationComponent;
this.addComponent(navigationComponent);
this.show(navigationComponentId);
this.activeComponentId = navigationComponentId;
});
}
public getComponentByName(name: string): NavigationComponent|null {
let foundComponent = null;
this.components.forEach((component: NavigationComponent) => {
if (component.getName() == name) {
foundComponent = component;
}
});
return foundComponent;
}
public toggle(): void {
......@@ -52,7 +87,7 @@ class NavigationContainer extends AbstractContainer {
this.parent.classList.remove('scaffold-content-navigation-expanded');
this.parent.classList.remove('scaffold-content-navigation-available');
if (hideSwitcher && this.switcher) {
$(this.switcher).hide();
this.switcher.style.display = 'none';
}
}
......@@ -81,14 +116,19 @@ class NavigationContainer extends AbstractContainer {
}
public show(component: string): void {
$(ScaffoldIdentifierEnum.contentNavigationDataComponent).hide();
this.container.querySelectorAll(ScaffoldIdentifierEnum.contentNavigationDataComponent).forEach((el: HTMLElement) => el.style.display = 'none');
if (typeof component !== undefined) {
this.parent.classList.add('scaffold-content-navigation-expanded');
this.parent.classList.add('scaffold-content-navigation-available');
$(ScaffoldIdentifierEnum.contentNavigation + ' [data-component="' + component + '"]').show();
const selectedElement = this.container.querySelector('[data-component="' + component + '"]') as HTMLElement;
if (selectedElement) {
// Re-set to the display setting from CSS
selectedElement.style.display = null;
}
}
if (this.switcher) {
$(this.switcher).show();
// Re-set to the display setting from CSS
this.switcher.style.display = null;
}
}
......@@ -103,20 +143,36 @@ class NavigationContainer extends AbstractContainer {
);
deferred.then((): void => {
this.parent.classList.add('scaffold-content-navigation-expanded');
$(ScaffoldIdentifierEnum.contentNavigationIframe).attr('src', urlToLoad);
const iFrameElement = this.getIFrameElement();
if (iFrameElement) {