Commit 571a5e8a authored by Benni Mack's avatar Benni Mack Committed by Richard Haeser

[FEATURE] Resizable navigation for all element / record selectors

This change adds a new Lit-based custom HTML component
called "<typo3-backend-navigation-switcher>" which moves
the current navigation frame resizable feature in a more flexible
re-usable way.

All RecordLinkHandlers and ElementBrowsers now have a
resizable and collapsible navigation area.

In addition, all logic is now decoupled from Viewport.ts

All ElementBrowser / RecordHandlers now also define
an inline style width to avoid jumping of the navigation area
within the modal box.

Further optimizations in this area:
* Remove the media queries for the CSS classes in the element browser?
* The newly introduced LIT element could be built as shadow DOM

Resolves: #93857
Releases: master
Change-Id: I66724086942e0778e05b9c26dd956bb8417af6d0
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/68692Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
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: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
parent cbe4455a
......@@ -137,6 +137,14 @@ $elementbrowser-breakpoint: 600px;
.list-tree-control > .fa:before {
background-color: #f2f2f2;
}
.scaffold-content-navigation-available & {
display: none;
}
.scaffold-content-navigation-available.scaffold-content-navigation-expanded & {
display: flex;
}
}
.element-browser-main-content {
......
......@@ -275,7 +275,6 @@ body {
&:after {
content: '';
position: absolute;
top: 0;
height: 100%;
width: 8px;
margin-left: -5px;
......@@ -350,3 +349,7 @@ body {
}
}
}
typo3-backend-navigation-switcher {
display: flex;
}
......@@ -20,7 +20,6 @@ export enum ScaffoldIdentifierEnum {
contentModuleIframe = '.t3js-scaffold-content-module-iframe',
contentNavigation = '.t3js-scaffold-content-navigation',
contentNavigationDataComponent = '.t3js-scaffold-content-navigation [data-component]',
contentNavigationDrag = '.t3js-scaffold-content-navigation-drag',
contentNavigationSwitcher = '.t3js-scaffold-content-navigation-switcher',
contentNavigationIframe = '.t3js-scaffold-content-navigation-iframe',
}
......@@ -12,11 +12,9 @@
*/
import ContentContainer = require('./Viewport/ContentContainer');
import {ScaffoldIdentifierEnum} from './Enum/Viewport/ScaffoldIdentifier';
import ConsumerScope = require('./Event/ConsumerScope');
import Loader = require('./Viewport/Loader');
import NavigationContainer = require('./Viewport/NavigationContainer');
import Persistent = require('./Storage/Persistent');
import Topbar = require('./Viewport/Topbar');
class Viewport {
......@@ -26,80 +24,11 @@ class Viewport {
public readonly NavigationContainer: NavigationContainer = null;
public readonly ContentContainer: ContentContainer = null;
public readonly consumerScope: any = ConsumerScope;
private document: HTMLDocument;
private readonly navigationDragHandler: HTMLElement;
constructor() {
this.document = document;
this.navigationDragHandler = <HTMLElement>document.querySelector(ScaffoldIdentifierEnum.contentNavigationDrag);
let navigationSwitcher = <HTMLElement>document.querySelector(ScaffoldIdentifierEnum.contentNavigationSwitcher);
this.Topbar = new Topbar();
this.NavigationContainer = new NavigationContainer(this.consumerScope, navigationSwitcher);
this.NavigationContainer = new NavigationContainer(this.consumerScope);
this.ContentContainer = new ContentContainer(this.consumerScope);
if (document.querySelector(ScaffoldIdentifierEnum.contentNavigation)) {
this.NavigationContainer.setWidth(<number>Persistent.get('navigation.width'));
}
window.addEventListener('resize', this.fallbackNavigationSizeIfNeeded, {passive: true});
if (navigationSwitcher) {
navigationSwitcher.addEventListener('mouseup', this.toggleNavigation, {passive: true});
navigationSwitcher.addEventListener('touchstart', this.toggleNavigation, {passive: true});
}
if (this.navigationDragHandler) {
this.navigationDragHandler.addEventListener('mousedown', this.startResizeNavigation, {passive: true});
this.navigationDragHandler.addEventListener('touchstart', this.startResizeNavigation, {passive: true});
}
}
private fallbackNavigationSizeIfNeeded = (event: UIEvent) => {
let window = <Window>event.currentTarget;
if (this.NavigationContainer.getWidth() === 0) {
return;
}
if (window.outerWidth < this.NavigationContainer.getWidth() + this.NavigationContainer.getPosition().left + 300) {
this.NavigationContainer.autoWidth();
}
}
private handleMouseMove = (event: MouseEvent) => {
this.resizeNavigation(<number>event.clientX);
}
private handleTouchMove = (event: TouchEvent) => {
this.resizeNavigation(<number>event.changedTouches[0].clientX);
}
private resizeNavigation = (position: number) => {
let width = Math.round(position) - Math.round(this.NavigationContainer.getPosition().left);
this.NavigationContainer.setWidth(width);
}
private stopResizeNavigation = () => {
this.navigationDragHandler.classList.remove('resizing');
this.document.removeEventListener('mousemove', this.handleMouseMove, false);
this.document.removeEventListener('mouseup', this.stopResizeNavigation, false);
this.document.removeEventListener('touchmove', this.handleTouchMove, false);
this.document.removeEventListener('touchend', this.stopResizeNavigation, false);
Persistent.set('navigation.width', <string><unknown>this.NavigationContainer.getWidth());
}
private startResizeNavigation = (event: MouseEvent | TouchEvent) => {
if (event instanceof MouseEvent && event.button === 2) {
return;
}
event.stopPropagation();
this.navigationDragHandler.classList.add('resizing');
this.document.addEventListener('mousemove', this.handleMouseMove, false);
this.document.addEventListener('mouseup', this.stopResizeNavigation, false);
this.document.addEventListener('touchmove', this.handleTouchMove, false);
this.document.addEventListener('touchend', this.stopResizeNavigation, false);
}
private toggleNavigation = (event: MouseEvent | TouchEvent) => {
if (event instanceof MouseEvent && event.button === 2) {
return;
}
event.stopPropagation();
this.NavigationContainer.toggle();
}
}
......
......@@ -22,12 +22,12 @@ class NavigationContainer extends AbstractContainer {
private readonly switcher: HTMLElement = null;
private activeComponentId: string = '';
public constructor(consumerScope: any, navigationSwitcher?: HTMLElement)
public constructor(consumerScope: any)
{
super(consumerScope);
this.parent = document.querySelector(ScaffoldIdentifierEnum.scaffold);
this.container = document.querySelector(ScaffoldIdentifierEnum.contentNavigation);
this.switcher = navigationSwitcher;
this.switcher = <HTMLElement>document.querySelector(ScaffoldIdentifierEnum.contentNavigationSwitcher);
}
/**
......@@ -84,10 +84,6 @@ class NavigationContainer extends AbstractContainer {
});
}
public toggle(): void {
this.parent.classList.toggle('scaffold-content-navigation-expanded');
}
public hide(hideSwitcher: boolean): void {
this.parent.classList.remove('scaffold-content-navigation-expanded');
this.parent.classList.remove('scaffold-content-navigation-available');
......@@ -96,30 +92,6 @@ class NavigationContainer extends AbstractContainer {
}
}
public getPosition(): DOMRect {
return this.container.getBoundingClientRect();
}
public getWidth(): number {
if (this.container) {
return <number>this.container.offsetWidth;
}
return 0;
}
public autoWidth(): void {
if (this.container) {
this.container.style.width = 'auto';
}
}
public setWidth(width: number): void {
width = width > 300 ? width : 300;
if (this.container) {
this.container.style.width = width + 'px';
}
}
public show(component: string): void {
this.container.querySelectorAll(ScaffoldIdentifierEnum.contentNavigationDataComponent).forEach((el: HTMLElement) => el.style.display = 'none');
if (typeof component !== undefined) {
......
/*
* 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 {html, customElement, property, internalProperty, eventOptions, LitElement, TemplateResult} from 'lit-element';
import {lll} from 'TYPO3/CMS/Core/lit-helper';
import Persistent = require('../Storage/Persistent');
import 'TYPO3/CMS/Backend/Element/IconElement';
const selectorConverter = {
fromAttribute(selector: string) {
return document.querySelector(selector);
}
};
@customElement('typo3-backend-navigation-switcher')
class ResizableNavigation extends LitElement {
@property({type: Number, attribute: 'minimum-width'}) minimumWidth: number = 250;
@property({type: Number, attribute: 'initial-width'}) initialWidth: number;
@property({type: String, attribute: 'persistence-identifier'}) persistenceIdentifier: string;
@property({attribute: 'parent', converter: selectorConverter}) parentContainer: HTMLElement;
@property({attribute: 'navigation', converter: selectorConverter}) navigationContainer: HTMLElement;
@internalProperty() resizing: boolean = false;
public connectedCallback(): void {
super.connectedCallback();
const initialWidth = this.initialWidth || parseInt(Persistent.get(this.persistenceIdentifier), 10);
this.setNavigationWidth(initialWidth);
window.addEventListener('resize', this.fallbackNavigationSizeIfNeeded, {passive: true});
}
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener('resize', this.fallbackNavigationSizeIfNeeded);
}
// disable shadow dom
protected createRenderRoot(): HTMLElement | ShadowRoot {
return this;
}
protected render(): TemplateResult {
return html`
<div class="scaffold-content-navigation-switcher">
<button @mouseup="${this.toggleNavigation}" @touchstart="${this.toggleNavigation}" class="btn btn-default btn-borderless scaffold-content-navigation-switcher-btn scaffold-content-navigation-switcher-open" role="button" title="${lll('viewport_navigation_show')}">
<typo3-backend-icon identifier="actions-chevron-right" size="small"></typo3-backend-icon>
</button>
<button @mouseup="${this.toggleNavigation}" @touchstart="${this.toggleNavigation}" class="btn btn-default btn-borderless scaffold-content-navigation-switcher-btn scaffold-content-navigation-switcher-close" role="button" title="${lll('viewport_navigation_hide')}">
<typo3-backend-icon identifier="actions-chevron-left" size="small"></typo3-backend-icon>
</button>
</div>
<div @mousedown="${this.startResizeNavigation}" @touchstart="${this.startResizeNavigation}" class="scaffold-content-navigation-drag ${this.resizing ? 'resizing' : ''}"></div>
`;
}
private toggleNavigation = (event: MouseEvent | TouchEvent) => {
if (event instanceof MouseEvent && event.button === 2) {
return;
}
event.stopPropagation();
this.parentContainer.classList.toggle('scaffold-content-navigation-expanded');
}
private fallbackNavigationSizeIfNeeded = (event: UIEvent) => {
let window = <Window>event.currentTarget;
if (this.getNavigationWidth() === 0) {
return;
}
if (window.outerWidth < this.getNavigationWidth() + this.getNavigationPosition().left + this.minimumWidth) {
this.autoNavigationWidth();
}
}
private handleMouseMove = (event: MouseEvent) => {
this.resizeNavigation(<number>event.clientX);
}
private handleTouchMove = (event: TouchEvent) => {
this.resizeNavigation(<number>event.changedTouches[0].clientX);
}
private resizeNavigation = (position: number) => {
let width = Math.round(position) - Math.round(this.getNavigationPosition().left);
this.setNavigationWidth(width);
}
//@eventOptions({passive: true})
private startResizeNavigation = (event: MouseEvent | TouchEvent) => {
if (event instanceof MouseEvent && event.button === 2) {
return;
}
event.stopPropagation();
this.resizing = true;
document.addEventListener('mousemove', this.handleMouseMove, false);
document.addEventListener('mouseup', this.stopResizeNavigation, false);
document.addEventListener('touchmove', this.handleTouchMove, false);
document.addEventListener('touchend', this.stopResizeNavigation, false);
}
private stopResizeNavigation = () => {
this.resizing = false;
document.removeEventListener('mousemove', this.handleMouseMove, false);
document.removeEventListener('mouseup', this.stopResizeNavigation, false);
document.removeEventListener('touchmove', this.handleTouchMove, false);
document.removeEventListener('touchend', this.stopResizeNavigation, false);
Persistent.set(this.persistenceIdentifier, <string><unknown>this.getNavigationWidth());
document.dispatchEvent(new CustomEvent('typo3:navigation:resized'));
}
private getNavigationPosition(): DOMRect {
return this.navigationContainer.getBoundingClientRect();
}
private getNavigationWidth(): number {
return <number>this.navigationContainer.offsetWidth;
}
private autoNavigationWidth(): void {
this.navigationContainer.style.width = 'auto';
}
private setNavigationWidth(width: number): void {
// Allow only 50% of the main document
const maxWidth = Math.round(this.parentContainer.getBoundingClientRect().width / 2);
if (width > maxWidth) {
width = maxWidth;
}
width = width > this.minimumWidth ? width : this.minimumWidth;
this.navigationContainer.style.width = width + 'px';
}
}
......@@ -129,6 +129,7 @@ class BackendController
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Notification');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/InfoWindow');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Viewport/ResizableNavigation');
// load the storage API and fill the UC into the PersistentStorage, so no additional AJAX call is needed
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Storage/Persistent', 'function(PersistentStorage) {
......
<div class="element-browser">
<div class="element-browser {f:if(condition: treeEnabled, then: 'scaffold-content-navigation-available scaffold-content-navigation-expanded')}">
<div class="element-browser-panel element-browser-main">
<f:if condition="{treeEnabled}">
<div class="element-browser-main-sidebar">
<div class="element-browser-main-sidebar" style="width: {initialNavigationWidth}px">
<div class="element-browser-body">
<f:render section="Tree"/>
</div>
</div>
<typo3-backend-navigation-switcher
parent=".element-browser"
navigation=".element-browser-main-sidebar"
minimum-width="250"
initial-width="{initialNavigationWidth}"
persistence-identifier="selector.navigation.width"
></typo3-backend-navigation-switcher>
</f:if>
<div class="element-browser-main-content">
<div class="element-browser-body">
......
......@@ -12,15 +12,12 @@
<iframe name="nav_frame" src="about:blank" id="typo3-navigationContainerIframe" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:iframe.navFrame')}" scrolling="no" class="scaffold-content-navigation-iframe t3js-scaffold-content-navigation-iframe"></iframe>
</div>
</div>
<div class="scaffold-content-navigation-switcher t3js-scaffold-content-navigation-switcher">
<button class="btn btn-default btn-borderless scaffold-content-navigation-switcher-btn scaffold-content-navigation-switcher-open" role="button" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_misc:viewport_navigation_show')}">
<core:icon identifier="actions-chevron-right" size="small" />
</button>
<button class="btn btn-default btn-borderless scaffold-content-navigation-switcher-btn scaffold-content-navigation-switcher-close" role="button" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_misc:viewport_navigation_hide')}">
<core:icon identifier="actions-chevron-left" size="small" />
</button>
</div>
<div class="scaffold-content-navigation-drag t3js-scaffold-content-navigation-drag"></div>
<typo3-backend-navigation-switcher
parent=".t3js-scaffold"
navigation=".t3js-scaffold-content-navigation"
minimum-width="300"
persistence-identifier="navigation.width"
></typo3-backend-navigation-switcher>
<div class="scaffold-content-module t3js-scaffold-content-module">
<iframe name="list_frame" id="typo3-contentIframe" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:iframe.listFrame')}" scrolling="no" class="scaffold-content-module-iframe t3js-scaffold-content-module-iframe"></iframe>
</div>
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports"],(function(t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.ScaffoldIdentifierEnum=void 0,function(t){t.scaffold=".t3js-scaffold",t.header=".t3js-scaffold-header",t.moduleMenu=".t3js-scaffold-modulemenu",t.content=".t3js-scaffold-content",t.contentModule=".t3js-scaffold-content-module",t.contentModuleIframe=".t3js-scaffold-content-module-iframe",t.contentNavigation=".t3js-scaffold-content-navigation",t.contentNavigationDataComponent=".t3js-scaffold-content-navigation [data-component]",t.contentNavigationDrag=".t3js-scaffold-content-navigation-drag",t.contentNavigationSwitcher=".t3js-scaffold-content-navigation-switcher",t.contentNavigationIframe=".t3js-scaffold-content-navigation-iframe"}(n.ScaffoldIdentifierEnum||(n.ScaffoldIdentifierEnum={}))}));
\ No newline at end of file
define(["require","exports"],(function(t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.ScaffoldIdentifierEnum=void 0,function(t){t.scaffold=".t3js-scaffold",t.header=".t3js-scaffold-header",t.moduleMenu=".t3js-scaffold-modulemenu",t.content=".t3js-scaffold-content",t.contentModule=".t3js-scaffold-content-module",t.contentModuleIframe=".t3js-scaffold-content-module-iframe",t.contentNavigation=".t3js-scaffold-content-navigation",t.contentNavigationDataComponent=".t3js-scaffold-content-navigation [data-component]",t.contentNavigationSwitcher=".t3js-scaffold-content-navigation-switcher",t.contentNavigationIframe=".t3js-scaffold-content-navigation-iframe"}(n.ScaffoldIdentifierEnum||(n.ScaffoldIdentifierEnum={}))}));
\ No newline at end of file
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","./Viewport/ContentContainer","./Enum/Viewport/ScaffoldIdentifier","./Event/ConsumerScope","./Viewport/Loader","./Viewport/NavigationContainer","./Storage/Persistent","./Viewport/Topbar"],(function(t,e,i,n,o,a,s,r,h){"use strict";class d{constructor(){this.Loader=a,this.NavigationContainer=null,this.ContentContainer=null,this.consumerScope=o,this.fallbackNavigationSizeIfNeeded=t=>{let e=t.currentTarget;0!==this.NavigationContainer.getWidth()&&e.outerWidth<this.NavigationContainer.getWidth()+this.NavigationContainer.getPosition().left+300&&this.NavigationContainer.autoWidth()},this.handleMouseMove=t=>{this.resizeNavigation(t.clientX)},this.handleTouchMove=t=>{this.resizeNavigation(t.changedTouches[0].clientX)},this.resizeNavigation=t=>{let e=Math.round(t)-Math.round(this.NavigationContainer.getPosition().left);this.NavigationContainer.setWidth(e)},this.stopResizeNavigation=()=>{this.navigationDragHandler.classList.remove("resizing"),this.document.removeEventListener("mousemove",this.handleMouseMove,!1),this.document.removeEventListener("mouseup",this.stopResizeNavigation,!1),this.document.removeEventListener("touchmove",this.handleTouchMove,!1),this.document.removeEventListener("touchend",this.stopResizeNavigation,!1),r.set("navigation.width",this.NavigationContainer.getWidth())},this.startResizeNavigation=t=>{t instanceof MouseEvent&&2===t.button||(t.stopPropagation(),this.navigationDragHandler.classList.add("resizing"),this.document.addEventListener("mousemove",this.handleMouseMove,!1),this.document.addEventListener("mouseup",this.stopResizeNavigation,!1),this.document.addEventListener("touchmove",this.handleTouchMove,!1),this.document.addEventListener("touchend",this.stopResizeNavigation,!1))},this.toggleNavigation=t=>{t instanceof MouseEvent&&2===t.button||(t.stopPropagation(),this.NavigationContainer.toggle())},this.document=document,this.navigationDragHandler=document.querySelector(n.ScaffoldIdentifierEnum.contentNavigationDrag);let t=document.querySelector(n.ScaffoldIdentifierEnum.contentNavigationSwitcher);this.Topbar=new h,this.NavigationContainer=new s(this.consumerScope,t),this.ContentContainer=new i(this.consumerScope),document.querySelector(n.ScaffoldIdentifierEnum.contentNavigation)&&this.NavigationContainer.setWidth(r.get("navigation.width")),window.addEventListener("resize",this.fallbackNavigationSizeIfNeeded,{passive:!0}),t&&(t.addEventListener("mouseup",this.toggleNavigation,{passive:!0}),t.addEventListener("touchstart",this.toggleNavigation,{passive:!0})),this.navigationDragHandler&&(this.navigationDragHandler.addEventListener("mousedown",this.startResizeNavigation,{passive:!0}),this.navigationDragHandler.addEventListener("touchstart",this.startResizeNavigation,{passive:!0}))}}let v;return top.TYPO3.Backend?v=top.TYPO3.Backend:(v=new d,top.TYPO3.Backend=v),v}));
\ No newline at end of file
define(["require","exports","./Viewport/ContentContainer","./Event/ConsumerScope","./Viewport/Loader","./Viewport/NavigationContainer","./Viewport/Topbar"],(function(n,t,e,o,i,r,a){"use strict";class s{constructor(){this.Loader=i,this.NavigationContainer=null,this.ContentContainer=null,this.consumerScope=o,this.Topbar=new a,this.NavigationContainer=new r(this.consumerScope),this.ContentContainer=new e(this.consumerScope)}}let c;return top.TYPO3.Backend?c=top.TYPO3.Backend:(c=new s,top.TYPO3.Backend=c),c}));
\ No newline at end of file
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","../Enum/Viewport/ScaffoldIdentifier","./AbstractContainer","../Event/TriggerRequest"],(function(t,e,n,i,o){"use strict";class a extends i.AbstractContainer{constructor(t,e){super(t),this.switcher=null,this.activeComponentId="",this.parent=document.querySelector(n.ScaffoldIdentifierEnum.scaffold),this.container=document.querySelector(n.ScaffoldIdentifierEnum.contentNavigation),this.switcher=e}showComponent(e){if(this.show(e),e===this.activeComponentId)return;if(""!==this.activeComponentId){let t=this.container.querySelector("#navigationComponent-"+this.activeComponentId.replace(/[/]/g,"_"));t&&(t.style.display="none")}const n="navigationComponent-"+e.replace(/[/]/g,"_");if(1===this.container.querySelectorAll('[data-component="'+e+'"]').length)return this.show(e),void(this.activeComponentId=e);t([e],t=>{if("string"==typeof t.navigationComponentName){const i=t.navigationComponentName,o=document.createElement(i);o.setAttribute("id",n),o.classList.add("scaffold-content-navigation-component"),o.dataset.component=e,this.container.append(o)}else{this.container.insertAdjacentHTML("beforeend",'<div class="scaffold-content-navigation-component" data-component="'+e+'" id="'+n+'"></div>');Object.values(t)[0].initialize("#"+n)}this.show(e),this.activeComponentId=e})}toggle(){this.parent.classList.toggle("scaffold-content-navigation-expanded")}hide(t){this.parent.classList.remove("scaffold-content-navigation-expanded"),this.parent.classList.remove("scaffold-content-navigation-available"),t&&this.switcher&&(this.switcher.style.display="none")}getPosition(){return this.container.getBoundingClientRect()}getWidth(){return this.container?this.container.offsetWidth:0}autoWidth(){this.container&&(this.container.style.width="auto")}setWidth(t){t=t>300?t:300,this.container&&(this.container.style.width=t+"px")}show(t){if(this.container.querySelectorAll(n.ScaffoldIdentifierEnum.contentNavigationDataComponent).forEach(t=>t.style.display="none"),void 0!==typeof t){this.parent.classList.add("scaffold-content-navigation-expanded"),this.parent.classList.add("scaffold-content-navigation-available");const e=this.container.querySelector('[data-component="'+t+'"]');e&&(e.style.display=null)}this.switcher&&(this.switcher.style.display=null)}setUrl(t,e){const n=this.consumerScope.invoke(new o("typo3.setUrl",e));return n.then(()=>{this.parent.classList.add("scaffold-content-navigation-expanded");const e=this.getIFrameElement();e&&e.setAttribute("src",t)}),n}getUrl(){const t=this.getIFrameElement();return t?t.getAttribute("src"):""}refresh(){const t=this.getIFrameElement();if(t)return t.contentWindow.location.reload()}getIFrameElement(){return this.container.querySelector(n.ScaffoldIdentifierEnum.contentNavigationIframe)}}return a}));
\ No newline at end of file
define(["require","exports","../Enum/Viewport/ScaffoldIdentifier","./AbstractContainer","../Event/TriggerRequest"],(function(t,e,n,i,o){"use strict";class a extends i.AbstractContainer{constructor(t){super(t),this.switcher=null,this.activeComponentId="",this.parent=document.querySelector(n.ScaffoldIdentifierEnum.scaffold),this.container=document.querySelector(n.ScaffoldIdentifierEnum.contentNavigation),this.switcher=document.querySelector(n.ScaffoldIdentifierEnum.contentNavigationSwitcher)}showComponent(e){if(this.show(e),e===this.activeComponentId)return;if(""!==this.activeComponentId){let t=this.container.querySelector("#navigationComponent-"+this.activeComponentId.replace(/[/]/g,"_"));t&&(t.style.display="none")}const n="navigationComponent-"+e.replace(/[/]/g,"_");if(1===this.container.querySelectorAll('[data-component="'+e+'"]').length)return this.show(e),void(this.activeComponentId=e);t([e],t=>{if("string"==typeof t.navigationComponentName){const i=t.navigationComponentName,o=document.createElement(i);o.setAttribute("id",n),o.classList.add("scaffold-content-navigation-component"),o.dataset.component=e,this.container.append(o)}else{this.container.insertAdjacentHTML("beforeend",'<div class="scaffold-content-navigation-component" data-component="'+e+'" id="'+n+'"></div>');Object.values(t)[0].initialize("#"+n)}this.show(e),this.activeComponentId=e})}hide(t){this.parent.classList.remove("scaffold-content-navigation-expanded"),this.parent.classList.remove("scaffold-content-navigation-available"),t&&this.switcher&&(this.switcher.style.display="none")}show(t){if(this.container.querySelectorAll(n.ScaffoldIdentifierEnum.contentNavigationDataComponent).forEach(t=>t.style.display="none"),void 0!==typeof t){this.parent.classList.add("scaffold-content-navigation-expanded"),this.parent.classList.add("scaffold-content-navigation-available");const e=this.container.querySelector('[data-component="'+t+'"]');e&&(e.style.display=null)}this.switcher&&(this.switcher.style.display=null)}setUrl(t,e){const n=this.consumerScope.invoke(new o("typo3.setUrl",e));return n.then(()=>{this.parent.classList.add("scaffold-content-navigation-expanded");const e=this.getIFrameElement();e&&e.setAttribute("src",t)}),n}getUrl(){const t=this.getIFrameElement();return t?t.getAttribute("src"):""}refresh(){const t=this.getIFrameElement();if(t)return t.contentWindow.location.reload()}getIFrameElement(){return this.container.querySelector(n.ScaffoldIdentifierEnum.contentNavigationIframe)}}return a}));
\ No newline at end of file
/*
* 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!
*/
var __decorate=this&&this.__decorate||function(t,e,i,n){var o,a=arguments.length,s=a<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,i,n);else for(var r=t.length-1;r>=0;r--)(o=t[r])&&(s=(a<3?o(s):a>3?o(e,i,s):o(e,i))||s);return a>3&&s&&Object.defineProperty(e,i,s),s};define(["require","exports","lit-element","TYPO3/CMS/Core/lit-helper","../Storage/Persistent","TYPO3/CMS/Backend/Element/IconElement"],(function(t,e,i,n,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0});const a={fromAttribute:t=>document.querySelector(t)};let s=class extends i.LitElement{constructor(){super(...arguments),this.minimumWidth=250,this.resizing=!1,this.toggleNavigation=t=>{t instanceof MouseEvent&&2===t.button||(t.stopPropagation(),this.parentContainer.classList.toggle("scaffold-content-navigation-expanded"))},this.fallbackNavigationSizeIfNeeded=t=>{let e=t.currentTarget;0!==this.getNavigationWidth()&&e.outerWidth<this.getNavigationWidth()+this.getNavigationPosition().left+this.minimumWidth&&this.autoNavigationWidth()},this.handleMouseMove=t=>{this.resizeNavigation(t.clientX)},this.handleTouchMove=t=>{this.resizeNavigation(t.changedTouches[0].clientX)},this.resizeNavigation=t=>{let e=Math.round(t)-Math.round(this.getNavigationPosition().left);this.setNavigationWidth(e)},this.startResizeNavigation=t=>{t instanceof MouseEvent&&2===t.button||(t.stopPropagation(),this.resizing=!0,document.addEventListener("mousemove",this.handleMouseMove,!1),document.addEventListener("mouseup",this.stopResizeNavigation,!1),document.addEventListener("touchmove",this.handleTouchMove,!1),document.addEventListener("touchend",this.stopResizeNavigation,!1))},this.stopResizeNavigation=()=>{this.resizing=!1,document.removeEventListener("mousemove",this.handleMouseMove,!1),document.removeEventListener("mouseup",this.stopResizeNavigation,!1),document.removeEventListener("touchmove",this.handleTouchMove,!1),document.removeEventListener("touchend",this.stopResizeNavigation,!1),o.set(this.persistenceIdentifier,this.getNavigationWidth()),document.dispatchEvent(new CustomEvent("typo3:navigation:resized"))}}connectedCallback(){super.connectedCallback();const t=this.initialWidth||parseInt(o.get(this.persistenceIdentifier),10);this.setNavigationWidth(t),window.addEventListener("resize",this.fallbackNavigationSizeIfNeeded,{passive:!0})}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("resize",this.fallbackNavigationSizeIfNeeded)}createRenderRoot(){return this}render(){return i.html`
<div class="scaffold-content-navigation-switcher">
<button @mouseup="${this.toggleNavigation}" @touchstart="${this.toggleNavigation}" class="btn btn-default btn-borderless scaffold-content-navigation-switcher-btn scaffold-content-navigation-switcher-open" role="button" title="${n.lll("viewport_navigation_show")}">
<typo3-backend-icon identifier="actions-chevron-right" size="small"></typo3-backend-icon>
</button>
<button @mouseup="${this.toggleNavigation}" @touchstart="${this.toggleNavigation}" class="btn btn-default btn-borderless scaffold-content-navigation-switcher-btn scaffold-content-navigation-switcher-close" role="button" title="${n.lll("viewport_navigation_hide")}">
<typo3-backend-icon identifier="actions-chevron-left" size="small"></typo3-backend-icon>
</button>
</div>
<div @mousedown="${this.startResizeNavigation}" @touchstart="${this.startResizeNavigation}" class="scaffold-content-navigation-drag ${this.resizing?"resizing":""}"></div>
`}getNavigationPosition(){return this.navigationContainer.getBoundingClientRect()}getNavigationWidth(){return this.navigationContainer.offsetWidth}autoNavigationWidth(){this.navigationContainer.style.width="auto"}setNavigationWidth(t){const e=Math.round(this.parentContainer.getBoundingClientRect().width/2);t>e&&(t=e),t=t>this.minimumWidth?t:this.minimumWidth,this.navigationContainer.style.width=t+"px"}};__decorate([i.property({type:Number,attribute:"minimum-width"})],s.prototype,"minimumWidth",void 0),__decorate([i.property({type:Number,attribute:"initial-width"})],s.prototype,"initialWidth",void 0),__decorate([i.property({type:String,attribute:"persistence-identifier"})],s.prototype,"persistenceIdentifier",void 0),__decorate([i.property({attribute:"parent",converter:a})],s.prototype,"parentContainer",void 0),__decorate([i.property({attribute:"navigation",converter:a})],s.prototype,"navigationContainer",void 0),__decorate([i.internalProperty()],s.prototype,"resizing",void 0),s=__decorate([i.customElement("typo3-backend-navigation-switcher")],s)}));
\ No newline at end of file
.. include:: ../../Includes.txt
===================================================================================
Feature: #93857 - Resizable navigation component for all element / record selectors
===================================================================================
See :issue:`93857`
Description
===========
The newly introduced possibility to resize and/or collapse the
navigation frame (e.g. Page Tree) in the main backend has been additionally
added to all Element Browser / Record Selectors, and Link Picker selections.
All modal areas with a Page Tree or File-based Folder Tree now
contain the same feature-set of collapsing / resizing, except
that the width is not installation-wide but is kept for the
main navigation area (initially 300 pixels) in a different place (set in the
Backend Users' "uc" `navigation.width` property) than for the element browser's
modal areas (initially 250 pixels, set in the Backend Users' "uc"
`selector.navigation.width` property).
A custom Lit-based web component is added, which is now re-used
in various places, and uses the same markup in all contexts.
Impact
======
Any backend user is now able to resize / collapse the navigation
area in the Record Selectors / Element Browser shipped with TYPO3 Core.
.. index:: Backend, JavaScript, ext:recordlist
......@@ -84,6 +84,7 @@ abstract class AbstractElementBrowser
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ElementBrowser');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Viewport/ResizableNavigation');
$this->initialize();
}
......
......@@ -116,6 +116,7 @@ class DatabaseBrowser extends AbstractElementBrowser implements ElementBrowserIn
'treeEnabled' => $withTree,
'temporaryTreeMountCancelUrl' => $this->getTemporaryTreeMountCancelNotice(),
'tree' => $tree,
'initialNavigationWidth' => $this->getBackendUser()->uc['selector']['navigation']['width'] ?? 250,
'content' => $renderedRecordList
]);
return $this->moduleTemplate->renderContent();
......
......@@ -207,6 +207,7 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
$view->assignMultiple([
'treeEnabled' => true,
'tree' => $tree,
'initialNavigationWidth' => $this->getBackendUser()->uc['selector']['navigation']['width'] ?? 250,
'content' => $files . $uploadForm . $createFolder
]);
return $this->moduleTemplate->renderContent();
......
......@@ -108,6 +108,7 @@ class FolderBrowser extends AbstractElementBrowser implements ElementBrowserInte