diff --git a/Build/Sources/TypeScript/backend/context-menu.ts b/Build/Sources/TypeScript/backend/context-menu.ts index 65dd7108ad0cd5b6c8bda757ed768f5a4fe21e7e..a80b2d8ca4a9287f4c9825d6250bead155821134 100644 --- a/Build/Sources/TypeScript/backend/context-menu.ts +++ b/Build/Sources/TypeScript/backend/context-menu.ts @@ -17,6 +17,7 @@ import ContextMenuActions from './context-menu-actions'; import DebounceEvent from '@typo3/core/event/debounce-event'; import RegularEvent from '@typo3/core/event/regular-event'; import { selector } from '@typo3/core/literals'; +import '@typo3/backend/element/spinner-element'; interface MousePosition { X: number; @@ -122,6 +123,7 @@ class ContextMenu { originalEvent: PointerEvent = null ): void { this.hideAll(); + this.initializeContextMenuContainer(); this.record = { table: table, uid: uid }; const focusableSource = eventSource.matches('a, button, [tabindex]:not([tabindex="-1"])') @@ -154,48 +156,49 @@ class ContextMenu { * Manipulates the DOM to add the divs needed for context menu at the bottom of the <body>-tag */ private initializeContextMenuContainer(): void { - if (document.querySelector('#contentMenu0') === null) { - const contextMenu1 = document.createElement('div'); - contextMenu1.classList.add('context-menu'); - contextMenu1.id = 'contentMenu0'; - contextMenu1.style.display = 'none'; - document.querySelector('body').append(contextMenu1); - - const contextMenu2 = document.createElement('div'); - contextMenu2.classList.add('context-menu'); - contextMenu2.id = 'contentMenu1'; - contextMenu2.style.display = 'none'; - contextMenu2.dataset.parent = '#contentMenu0' - document.querySelector('body').append(contextMenu2); - - document.querySelectorAll('.context-menu').forEach((contextMenu: Element): void => { - // Explicitly update cursor position if element is entered to avoid timing issues - new RegularEvent('mouseenter', (event: MouseEvent): void => { - this.storeMousePosition(event); - }).bindTo(contextMenu); - - new DebounceEvent('mouseleave', (event: MouseEvent) => { - const target: HTMLElement = event.target as HTMLElement; - const childMenu: HTMLElement | null = document.querySelector(selector`[data-parent="#${target.id}"]`); - - const hideThisMenu = - !ContextMenu.within(target, this.mousePos.X, this.mousePos.Y) // cursor it outside triggered context menu - && (childMenu === null || childMenu.offsetParent === null); // child menu, if any, is not visible - - if (hideThisMenu) { - this.hide(target); - - // close parent menu (if any) if cursor is outside its boundaries - let parent: HTMLElement | null; - if (typeof target.dataset.parent !== 'undefined' && (parent = document.querySelector(target.dataset.parent)) !== null) { - if (!ContextMenu.within(parent, this.mousePos.X, this.mousePos.Y)) { - this.hide(document.querySelector(target.dataset.parent)); - } + if (document.querySelector('#contentMenu0') !== null) { + return; + } + const contextMenu1 = document.createElement('div'); + contextMenu1.classList.add('context-menu'); + contextMenu1.id = 'contentMenu0'; + contextMenu1.style.display = 'none'; + document.querySelector('body').append(contextMenu1); + + const contextMenu2 = document.createElement('div'); + contextMenu2.classList.add('context-menu'); + contextMenu2.id = 'contentMenu1'; + contextMenu2.style.display = 'none'; + contextMenu2.dataset.parent = '#contentMenu0' + document.querySelector('body').append(contextMenu2); + + document.querySelectorAll('.context-menu').forEach((contextMenu: Element): void => { + // Explicitly update cursor position if element is entered to avoid timing issues + new RegularEvent('mouseenter', (event: MouseEvent): void => { + this.storeMousePosition(event); + }).bindTo(contextMenu); + + new DebounceEvent('mouseleave', (event: MouseEvent) => { + const target: HTMLElement = event.target as HTMLElement; + const childMenu: HTMLElement | null = document.querySelector(selector`[data-parent="#${target.id}"]`); + + const hideThisMenu = + !ContextMenu.within(target, this.mousePos.X, this.mousePos.Y) // cursor it outside triggered context menu + && (childMenu === null || childMenu.offsetParent === null); // child menu, if any, is not visible + + if (hideThisMenu) { + this.hide(target); + + // close parent menu (if any), if cursor is outside its boundaries + let parent: HTMLElement | null; + if (typeof target.dataset.parent !== 'undefined' && (parent = document.querySelector(target.dataset.parent)) !== null) { + if (!ContextMenu.within(parent, this.mousePos.X, this.mousePos.Y)) { + this.hide(document.querySelector(target.dataset.parent)); } } - }, 500).bindTo(contextMenu); - }); - } + } + }, 500).bindTo(contextMenu); + }); } private handleTriggerEvent(event: PointerEvent): void @@ -237,28 +240,44 @@ class ContextMenu { * Make the AJAX request * * @param {string} parameters Parameters sent to the server + * @param {MousePosition} position */ private fetch(parameters: string, position: MousePosition): void { + const stubMenu = this.renderStubMenu(0, position); const url = TYPO3.settings.ajaxUrls.contextmenu; (new AjaxRequest(url)).withQueryArguments(parameters).get().then(async (response: AjaxResponse): Promise<void> => { const data: MenuItems = await response.resolve(); if (typeof response !== 'undefined' && Object.keys(response).length > 0) { - this.populateData(data, 0, position); + this.populateData(data, 0); } + }).catch((): void => { + this.hide(stubMenu); }); } + private renderStubMenu(level: number, position: MousePosition): HTMLElement|null { + const contentMenuCurrent = document.querySelector('#contentMenu' + level) as HTMLElement; + if (contentMenuCurrent !== null) { + contentMenuCurrent.replaceChildren(document.createRange().createContextualFragment('<typo3-backend-spinner size="medium"></typo3-backend-spinner>')); + contentMenuCurrent.style.display = null; + position ??= this.getPosition(contentMenuCurrent); + const coordinates = this.toPixel(position); + + contentMenuCurrent.style.top = coordinates.top; + contentMenuCurrent.style.insetInlineStart = coordinates.start; + } + + return contentMenuCurrent; + } + /** * Fills the context menu with content and displays it correctly * depending on the mouse position * * @param {MenuItems} items The data that will be put in the menu * @param {number} level The depth of the context menu - * @param {MousPosition} */ - private populateData(items: MenuItems, level: number, position: MousePosition): void { - this.initializeContextMenuContainer(); - + private populateData(items: MenuItems, level: number): void { const contentMenuCurrent = document.querySelector('#contentMenu' + level) as HTMLElement; const contentMenuParent = document.querySelector('#contentMenu' + (level - 1)) as HTMLElement; if (contentMenuCurrent !== null && contentMenuParent?.offsetParent !== null) { @@ -272,11 +291,6 @@ class ContextMenu { contentMenuCurrent.innerHTML = ''; contentMenuCurrent.appendChild(menuGroup); contentMenuCurrent.style.display = null; - position ??= this.getPosition(contentMenuCurrent); - const coordinates = this.toPixel(position); - - contentMenuCurrent.style.top = coordinates.top; - contentMenuCurrent.style.insetInlineStart = coordinates.start; (contentMenuCurrent.querySelector('.context-menu-item[tabindex="-1"]') as HTMLElement).focus(); this.initializeEvents(contentMenuCurrent, level); } diff --git a/Build/Sources/TypeScript/filelist/file-list-actions.ts b/Build/Sources/TypeScript/filelist/file-list-actions.ts index ef4c74720d97c0cd2932aaff477acff107cff34a..5f005c01bbb2f84a67e635a9777a40502a8cef38 100644 --- a/Build/Sources/TypeScript/filelist/file-list-actions.ts +++ b/Build/Sources/TypeScript/filelist/file-list-actions.ts @@ -70,6 +70,8 @@ class FileListActions { constructor() { new RegularEvent('contextmenu', (event: Event, target: HTMLElement): void => { event.preventDefault(); + event.stopImmediatePropagation(); + const detail: FileListActionDetail = this.getActionDetail(event, target); switch (detail.action) { case 'primary': diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/context-menu.js b/typo3/sysext/backend/Resources/Public/JavaScript/context-menu.js index 33f13dbce778a49a22161e355ffd815fbb8fb1bd..cc569ab645a8ce487d35cc58fc4aece7cdde92a3 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/context-menu.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/context-menu.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import ContextMenuActions from"@typo3/backend/context-menu-actions.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import RegularEvent from"@typo3/core/event/regular-event.js";import{selector}from"@typo3/core/literals.js";class ContextMenu{constructor(){this.mousePos={X:null,Y:null},this.record={uid:null,table:null},this.eventSources=[],document.addEventListener("click",(e=>{this.handleTriggerEvent(e)})),document.addEventListener("contextmenu",(e=>{this.handleTriggerEvent(e)}))}static drawActionItem(e){const t=document.createElement("li");t.role="menuitem",t.classList.add("context-menu-item"),t.dataset.callbackAction=e.callbackAction,t.tabIndex=-1;const n=e.additionalAttributes||{};for(const e of Object.entries(n)){const[n,o]=e;t.setAttribute(n,o)}const o=document.createElement("span");o.classList.add("context-menu-item-icon"),o.innerHTML=e.icon;const s=document.createElement("span");return s.classList.add("context-menu-item-label"),s.innerHTML=e.label,t.append(o),t.append(s),t}static within(e,t,n){const o=e.getBoundingClientRect(),s=window.pageXOffset||document.documentElement.scrollLeft,i=window.pageYOffset||document.documentElement.scrollTop,c=t>=o.left+s&&t<=o.left+s+o.width,a=n>=o.top+i&&n<=o.top+i+o.height;return c&&a}show(e,t,n,o,s,i=null,c=null){this.hideAll(),this.record={table:e,uid:t};const a=i.matches('a, button, [tabindex]:not([tabindex="-1"])')?i:i.closest('a, button, [tabindex]:not([tabindex="-1"])');!1===this.eventSources.includes(a)&&this.eventSources.push(a);const r=new URLSearchParams;void 0!==e&&r.set("table",e),void 0!==t&&r.set("uid",t.toString()),void 0!==n&&r.set("context",n);let l=null;null!==c&&(this.storeMousePosition(c),l=this.mousePos),this.fetch(r.toString(),l)}initializeContextMenuContainer(){if(null===document.querySelector("#contentMenu0")){const e=document.createElement("div");e.classList.add("context-menu"),e.id="contentMenu0",e.style.display="none",document.querySelector("body").append(e);const t=document.createElement("div");t.classList.add("context-menu"),t.id="contentMenu1",t.style.display="none",t.dataset.parent="#contentMenu0",document.querySelector("body").append(t),document.querySelectorAll(".context-menu").forEach((e=>{new RegularEvent("mouseenter",(e=>{this.storeMousePosition(e)})).bindTo(e),new DebounceEvent("mouseleave",(e=>{const t=e.target,n=document.querySelector(selector`[data-parent="#${t.id}"]`);if(!ContextMenu.within(t,this.mousePos.X,this.mousePos.Y)&&(null===n||null===n.offsetParent)){let e;this.hide(t),void 0!==t.dataset.parent&&null!==(e=document.querySelector(t.dataset.parent))&&(ContextMenu.within(e,this.mousePos.X,this.mousePos.Y)||this.hide(document.querySelector(t.dataset.parent)))}}),500).bindTo(e)}))}}handleTriggerEvent(e){if(!(e.target instanceof Element))return;const t=e.target.closest("[data-contextmenu-trigger]");if(t instanceof HTMLElement)return void this.handleContextMenuEvent(e,t);e.target.closest(".context-menu")||this.hideAll()}handleContextMenuEvent(e,t){const n=t.dataset.contextmenuTrigger;"click"!==n&&n!==e.type||(e.preventDefault(),this.show(t.dataset.contextmenuTable??"",t.dataset.contextmenuUid??"",t.dataset.contextmenuContext??"","","",t,e))}fetch(e,t){const n=TYPO3.settings.ajaxUrls.contextmenu;new AjaxRequest(n).withQueryArguments(e).get().then((async e=>{const n=await e.resolve();void 0!==e&&Object.keys(e).length>0&&this.populateData(n,0,t)}))}populateData(e,t,n){this.initializeContextMenuContainer();const o=document.querySelector("#contentMenu"+t),s=document.querySelector("#contentMenu"+(t-1));if(null!==o&&null!==s?.offsetParent){const s=document.createElement("ul");s.classList.add("context-menu-group"),s.role="menu",this.drawMenu(e,t).forEach((e=>{s.appendChild(e)})),o.innerHTML="",o.appendChild(s),o.style.display=null,n??(n=this.getPosition(o));const i=this.toPixel(n);o.style.top=i.top,o.style.insetInlineStart=i.start,o.querySelector('.context-menu-item[tabindex="-1"]').focus(),this.initializeEvents(o,t)}}initializeEvents(e,t){e.querySelectorAll("li.context-menu-item").forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const n=e.currentTarget;if(n.classList.contains("context-menu-item-submenu"))return void this.openSubmenu(t,n);const{callbackAction:o,callbackModule:s,...i}=n.dataset;n.dataset.callbackModule?import(s+".js").then((({default:e})=>{e[o](this.record.table,this.record.uid,i)})):ContextMenuActions&&"function"==typeof ContextMenuActions[o]?ContextMenuActions[o](this.record.table,this.record.uid,i):console.error("action: "+o+" not found"),this.hideAll()})),e.addEventListener("keydown",(e=>{e.preventDefault();const n=e.target;switch(e.key){case"Down":case"ArrowDown":this.setFocusToNextItem(n);break;case"Up":case"ArrowUp":this.setFocusToPreviousItem(n);break;case"Right":case"ArrowRight":if(!n.classList.contains("context-menu-item-submenu"))return;this.openSubmenu(t,n);break;case"Home":this.setFocusToFirstItem(n);break;case"End":this.setFocusToLastItem(n);break;case"Enter":case"Space":n.click();break;case"Esc":case"Escape":case"Left":case"ArrowLeft":this.hide(n.closest(".context-menu"));break;case"Tab":this.hideAll();break;default:return}}))}))}setFocusToPreviousItem(e){let t=this.getItemBackward(e.previousElementSibling);t||(t=this.getLastItem(e)),t.focus()}setFocusToNextItem(e){let t=this.getItemForward(e.nextElementSibling);t||(t=this.getFirstItem(e)),t.focus()}setFocusToFirstItem(e){const t=this.getFirstItem(e);t&&t.focus()}setFocusToLastItem(e){const t=this.getLastItem(e);t&&t.focus()}getItemBackward(e){for(;e&&(!e.classList.contains("context-menu-item")||"-1"!==e.getAttribute("tabindex"));)e=e.previousElementSibling;return e}getItemForward(e){for(;e&&(!e.classList.contains("context-menu-item")||"-1"!==e.getAttribute("tabindex"));)e=e.nextElementSibling;return e}getFirstItem(e){return this.getItemForward(e.parentElement.firstElementChild)}getLastItem(e){return this.getItemBackward(e.parentElement.lastElementChild)}openSubmenu(e,t){!1===this.eventSources.includes(t)&&this.eventSources.push(t);const n=document.querySelector("#contentMenu"+(e+1));n.innerHTML="",n.appendChild(t.nextElementSibling.querySelector(".context-menu-group").cloneNode(!0)),n.style.display=null;const o=this.toPixel(this.getPosition(n));n.style.top=o.top,n.style.insetInlineStart=o.start,n.querySelector('.context-menu-item[tabindex="-1"]').focus(),this.initializeEvents(n,e)}toPixel(e){return{start:Math.round(e.X)+"px",top:Math.round(e.Y)+"px"}}getPosition(e){const t="rtl"===document.querySelector("html").dir?"rtl":"ltr",n=this.eventSources?.[this.eventSources.length-1],o=e.offsetWidth,s=e.offsetHeight,i=window.innerWidth,c=window.innerHeight;let a=0,r=0;if(null!=n){const e=n.getBoundingClientRect();r=e.y,a="ltr"===t?e.x+e.width:i-e.x,n.classList.contains("context-menu-item-submenu")&&(r-=8)}else r=this.mousePos.Y,a="ltr"===t?this.mousePos.X:i-this.mousePos.X;return r+s+10+5<c?r+=5:r=c-s-10,a+o+10+5<i?a+=5:a=i-o-10,{X:a,Y:r}}drawMenu(e,t){const n=[];for(const o of Object.values(e))if("item"===o.type)n.push(ContextMenu.drawActionItem(o));else if("divider"===o.type){const e=document.createElement("li");e.role="separator",e.classList.add("context-menu-divider"),n.push(e)}else if("submenu"===o.type||o.childItems){const e=document.createElement("li");e.role="menuitem",e.ariaHasPopup="true",e.classList.add("context-menu-item","context-menu-item-submenu"),e.tabIndex=-1;const s=document.createElement("span");s.classList.add("context-menu-item-icon"),s.innerHTML=o.icon,e.appendChild(s);const i=document.createElement("span");i.classList.add("context-menu-item-label"),i.innerHTML=o.label,e.appendChild(i);const c=document.createElement("span");c.classList.add("context-menu-item-indicator"),c.innerHTML='<typo3-backend-icon identifier="actions-chevron-'+("rtl"===document.querySelector("html").dir?"left":"right")+'" size="small"></typo3-backend-icon>',e.appendChild(c),n.push(e);const a=document.createElement("div");a.classList.add("context-menu","contentMenu"+(t+1)),a.style.display="none";const r=document.createElement("ul");r.role="menu",r.classList.add("context-menu-group"),this.drawMenu(o.childItems,1).forEach((e=>{r.appendChild(e)})),a.appendChild(r),n.push(a)}return n}storeMousePosition(e){this.mousePos={X:e.pageX,Y:e.pageY}}hide(e){if(null===e)return;e.style.top=null,e.style.insetInlineStart=null,e.style.display="none";const t=this.eventSources.pop();t&&t.focus()}hideAll(){this.hide(document.querySelector("#contentMenu0")),this.hide(document.querySelector("#contentMenu1"))}}export default new ContextMenu; \ No newline at end of file +import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import ContextMenuActions from"@typo3/backend/context-menu-actions.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import RegularEvent from"@typo3/core/event/regular-event.js";import{selector}from"@typo3/core/literals.js";import"@typo3/backend/element/spinner-element.js";class ContextMenu{constructor(){this.mousePos={X:null,Y:null},this.record={uid:null,table:null},this.eventSources=[],document.addEventListener("click",(e=>{this.handleTriggerEvent(e)})),document.addEventListener("contextmenu",(e=>{this.handleTriggerEvent(e)}))}static drawActionItem(e){const t=document.createElement("li");t.role="menuitem",t.classList.add("context-menu-item"),t.dataset.callbackAction=e.callbackAction,t.tabIndex=-1;const n=e.additionalAttributes||{};for(const e of Object.entries(n)){const[n,o]=e;t.setAttribute(n,o)}const o=document.createElement("span");o.classList.add("context-menu-item-icon"),o.innerHTML=e.icon;const s=document.createElement("span");return s.classList.add("context-menu-item-label"),s.innerHTML=e.label,t.append(o),t.append(s),t}static within(e,t,n){const o=e.getBoundingClientRect(),s=window.pageXOffset||document.documentElement.scrollLeft,i=window.pageYOffset||document.documentElement.scrollTop,c=t>=o.left+s&&t<=o.left+s+o.width,r=n>=o.top+i&&n<=o.top+i+o.height;return c&&r}show(e,t,n,o,s,i=null,c=null){this.hideAll(),this.initializeContextMenuContainer(),this.record={table:e,uid:t};const r=i.matches('a, button, [tabindex]:not([tabindex="-1"])')?i:i.closest('a, button, [tabindex]:not([tabindex="-1"])');!1===this.eventSources.includes(r)&&this.eventSources.push(r);const a=new URLSearchParams;void 0!==e&&a.set("table",e),void 0!==t&&a.set("uid",t.toString()),void 0!==n&&a.set("context",n);let l=null;null!==c&&(this.storeMousePosition(c),l=this.mousePos),this.fetch(a.toString(),l)}initializeContextMenuContainer(){if(null!==document.querySelector("#contentMenu0"))return;const e=document.createElement("div");e.classList.add("context-menu"),e.id="contentMenu0",e.style.display="none",document.querySelector("body").append(e);const t=document.createElement("div");t.classList.add("context-menu"),t.id="contentMenu1",t.style.display="none",t.dataset.parent="#contentMenu0",document.querySelector("body").append(t),document.querySelectorAll(".context-menu").forEach((e=>{new RegularEvent("mouseenter",(e=>{this.storeMousePosition(e)})).bindTo(e),new DebounceEvent("mouseleave",(e=>{const t=e.target,n=document.querySelector(selector`[data-parent="#${t.id}"]`);if(!ContextMenu.within(t,this.mousePos.X,this.mousePos.Y)&&(null===n||null===n.offsetParent)){let e;this.hide(t),void 0!==t.dataset.parent&&null!==(e=document.querySelector(t.dataset.parent))&&(ContextMenu.within(e,this.mousePos.X,this.mousePos.Y)||this.hide(document.querySelector(t.dataset.parent)))}}),500).bindTo(e)}))}handleTriggerEvent(e){if(!(e.target instanceof Element))return;const t=e.target.closest("[data-contextmenu-trigger]");if(t instanceof HTMLElement)return void this.handleContextMenuEvent(e,t);e.target.closest(".context-menu")||this.hideAll()}handleContextMenuEvent(e,t){const n=t.dataset.contextmenuTrigger;"click"!==n&&n!==e.type||(e.preventDefault(),this.show(t.dataset.contextmenuTable??"",t.dataset.contextmenuUid??"",t.dataset.contextmenuContext??"","","",t,e))}fetch(e,t){const n=this.renderStubMenu(0,t),o=TYPO3.settings.ajaxUrls.contextmenu;new AjaxRequest(o).withQueryArguments(e).get().then((async e=>{const t=await e.resolve();void 0!==e&&Object.keys(e).length>0&&this.populateData(t,0)})).catch((()=>{this.hide(n)}))}renderStubMenu(e,t){const n=document.querySelector("#contentMenu"+e);if(null!==n){n.replaceChildren(document.createRange().createContextualFragment('<typo3-backend-spinner size="medium"></typo3-backend-spinner>')),n.style.display=null,t??(t=this.getPosition(n));const e=this.toPixel(t);n.style.top=e.top,n.style.insetInlineStart=e.start}return n}populateData(e,t){const n=document.querySelector("#contentMenu"+t),o=document.querySelector("#contentMenu"+(t-1));if(null!==n&&null!==o?.offsetParent){const o=document.createElement("ul");o.classList.add("context-menu-group"),o.role="menu",this.drawMenu(e,t).forEach((e=>{o.appendChild(e)})),n.innerHTML="",n.appendChild(o),n.style.display=null,n.querySelector('.context-menu-item[tabindex="-1"]').focus(),this.initializeEvents(n,t)}}initializeEvents(e,t){e.querySelectorAll("li.context-menu-item").forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const n=e.currentTarget;if(n.classList.contains("context-menu-item-submenu"))return void this.openSubmenu(t,n);const{callbackAction:o,callbackModule:s,...i}=n.dataset;n.dataset.callbackModule?import(s+".js").then((({default:e})=>{e[o](this.record.table,this.record.uid,i)})):ContextMenuActions&&"function"==typeof ContextMenuActions[o]?ContextMenuActions[o](this.record.table,this.record.uid,i):console.error("action: "+o+" not found"),this.hideAll()})),e.addEventListener("keydown",(e=>{e.preventDefault();const n=e.target;switch(e.key){case"Down":case"ArrowDown":this.setFocusToNextItem(n);break;case"Up":case"ArrowUp":this.setFocusToPreviousItem(n);break;case"Right":case"ArrowRight":if(!n.classList.contains("context-menu-item-submenu"))return;this.openSubmenu(t,n);break;case"Home":this.setFocusToFirstItem(n);break;case"End":this.setFocusToLastItem(n);break;case"Enter":case"Space":n.click();break;case"Esc":case"Escape":case"Left":case"ArrowLeft":this.hide(n.closest(".context-menu"));break;case"Tab":this.hideAll();break;default:return}}))}))}setFocusToPreviousItem(e){let t=this.getItemBackward(e.previousElementSibling);t||(t=this.getLastItem(e)),t.focus()}setFocusToNextItem(e){let t=this.getItemForward(e.nextElementSibling);t||(t=this.getFirstItem(e)),t.focus()}setFocusToFirstItem(e){const t=this.getFirstItem(e);t&&t.focus()}setFocusToLastItem(e){const t=this.getLastItem(e);t&&t.focus()}getItemBackward(e){for(;e&&(!e.classList.contains("context-menu-item")||"-1"!==e.getAttribute("tabindex"));)e=e.previousElementSibling;return e}getItemForward(e){for(;e&&(!e.classList.contains("context-menu-item")||"-1"!==e.getAttribute("tabindex"));)e=e.nextElementSibling;return e}getFirstItem(e){return this.getItemForward(e.parentElement.firstElementChild)}getLastItem(e){return this.getItemBackward(e.parentElement.lastElementChild)}openSubmenu(e,t){!1===this.eventSources.includes(t)&&this.eventSources.push(t);const n=document.querySelector("#contentMenu"+(e+1));n.innerHTML="",n.appendChild(t.nextElementSibling.querySelector(".context-menu-group").cloneNode(!0)),n.style.display=null;const o=this.toPixel(this.getPosition(n));n.style.top=o.top,n.style.insetInlineStart=o.start,n.querySelector('.context-menu-item[tabindex="-1"]').focus(),this.initializeEvents(n,e)}toPixel(e){return{start:Math.round(e.X)+"px",top:Math.round(e.Y)+"px"}}getPosition(e){const t="rtl"===document.querySelector("html").dir?"rtl":"ltr",n=this.eventSources?.[this.eventSources.length-1],o=e.offsetWidth,s=e.offsetHeight,i=window.innerWidth,c=window.innerHeight;let r=0,a=0;if(null!=n){const e=n.getBoundingClientRect();a=e.y,r="ltr"===t?e.x+e.width:i-e.x,n.classList.contains("context-menu-item-submenu")&&(a-=8)}else a=this.mousePos.Y,r="ltr"===t?this.mousePos.X:i-this.mousePos.X;return a+s+10+5<c?a+=5:a=c-s-10,r+o+10+5<i?r+=5:r=i-o-10,{X:r,Y:a}}drawMenu(e,t){const n=[];for(const o of Object.values(e))if("item"===o.type)n.push(ContextMenu.drawActionItem(o));else if("divider"===o.type){const e=document.createElement("li");e.role="separator",e.classList.add("context-menu-divider"),n.push(e)}else if("submenu"===o.type||o.childItems){const e=document.createElement("li");e.role="menuitem",e.ariaHasPopup="true",e.classList.add("context-menu-item","context-menu-item-submenu"),e.tabIndex=-1;const s=document.createElement("span");s.classList.add("context-menu-item-icon"),s.innerHTML=o.icon,e.appendChild(s);const i=document.createElement("span");i.classList.add("context-menu-item-label"),i.innerHTML=o.label,e.appendChild(i);const c=document.createElement("span");c.classList.add("context-menu-item-indicator"),c.innerHTML='<typo3-backend-icon identifier="actions-chevron-'+("rtl"===document.querySelector("html").dir?"left":"right")+'" size="small"></typo3-backend-icon>',e.appendChild(c),n.push(e);const r=document.createElement("div");r.classList.add("context-menu","contentMenu"+(t+1)),r.style.display="none";const a=document.createElement("ul");a.role="menu",a.classList.add("context-menu-group"),this.drawMenu(o.childItems,1).forEach((e=>{a.appendChild(e)})),r.appendChild(a),n.push(r)}return n}storeMousePosition(e){this.mousePos={X:e.pageX,Y:e.pageY}}hide(e){if(null===e)return;e.style.top=null,e.style.insetInlineStart=null,e.style.display="none";const t=this.eventSources.pop();t&&t.focus()}hideAll(){this.hide(document.querySelector("#contentMenu0")),this.hide(document.querySelector("#contentMenu1"))}}export default new ContextMenu; \ No newline at end of file diff --git a/typo3/sysext/filelist/Resources/Public/JavaScript/file-list-actions.js b/typo3/sysext/filelist/Resources/Public/JavaScript/file-list-actions.js index 32ffbc347d29cd0eab6ea8704dc7ec53eaa27972..ff9aa17bb2e55bc80b13dd6c079ca88c89d08406 100644 --- a/typo3/sysext/filelist/Resources/Public/JavaScript/file-list-actions.js +++ b/typo3/sysext/filelist/Resources/Public/JavaScript/file-list-actions.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import RegularEvent from"@typo3/core/event/regular-event.js";export var FileListActionEvent;!function(e){e.primary="typo3:filelist:resource:action:primary",e.primaryContextmenu="typo3:filelist:resource:action:primaryContextmenu",e.show="typo3:filelist:resource:action:show",e.rename="typo3:filelist:resource:action:rename",e.select="typo3:filelist:resource:action:select",e.download="typo3:filelist:resource:action:download",e.updateOnlineMedia="typo3:filelist:resource:action:updateOnlineMedia"}(FileListActionEvent||(FileListActionEvent={}));export var FileListActionSelector;!function(e){e.elementSelector="[data-filelist-element]",e.actionSelector="[data-filelist-action]"}(FileListActionSelector||(FileListActionSelector={}));export class FileListActionUtility{static createResourceFromContextDataset(e){return{type:e.filecontextType,identifier:e.filecontextIdentifier,name:e.filecontextName,thumbnail:null,uid:e.filecontextUid?parseInt(e.filecontextUid,10):null,metaUid:e.filecontextMetaUid?parseInt(e.filecontextMetaUid,10):null}}static getResourceForElement(e){return{type:e.dataset.filelistType,identifier:e.dataset.filelistIdentifier,name:e.dataset.filelistName,thumbnail:"filelistThumbnail"in e.dataset&&""!==e.dataset.filelistThumbnail.trim()?e.dataset.filelistThumbnail:null,uid:e.dataset.filelistUid?parseInt(e.dataset.filelistUid,10):null,metaUid:e.dataset.filelistMetaUid?parseInt(e.dataset.filelistMetaUid,10):null}}}class FileListActions{constructor(){new RegularEvent("contextmenu",((e,t)=>{e.preventDefault();const i=this.getActionDetail(e,t);if("primary"===i.action)document.dispatchEvent(new CustomEvent(FileListActionEvent.primaryContextmenu,{detail:i}))})).delegateTo(document,FileListActionSelector.actionSelector),new RegularEvent("click",((e,t)=>{e.preventDefault();const i=this.getActionDetail(e,t);switch(i.action){case"primary":document.dispatchEvent(new CustomEvent(FileListActionEvent.primary,{detail:i}));break;case"show":document.dispatchEvent(new CustomEvent(FileListActionEvent.show,{detail:i}));break;case"select":document.dispatchEvent(new CustomEvent(FileListActionEvent.select,{detail:i}));break;case"rename":document.dispatchEvent(new CustomEvent(FileListActionEvent.rename,{detail:i}));break;case"download":document.dispatchEvent(new CustomEvent(FileListActionEvent.download,{detail:i}));break;case"updateOnlineMedia":document.dispatchEvent(new CustomEvent(FileListActionEvent.updateOnlineMedia,{detail:i}))}})).delegateTo(document,FileListActionSelector.actionSelector)}getActionDetail(e,t){const i=t.dataset.filelistAction,n=t.closest(FileListActionSelector.elementSelector);return{event:e,trigger:t,action:i,resources:[FileListActionUtility.getResourceForElement(n)],url:t.dataset.filelistActionUrl??null}}}export default new FileListActions; \ No newline at end of file +import RegularEvent from"@typo3/core/event/regular-event.js";export var FileListActionEvent;!function(e){e.primary="typo3:filelist:resource:action:primary",e.primaryContextmenu="typo3:filelist:resource:action:primaryContextmenu",e.show="typo3:filelist:resource:action:show",e.rename="typo3:filelist:resource:action:rename",e.select="typo3:filelist:resource:action:select",e.download="typo3:filelist:resource:action:download",e.updateOnlineMedia="typo3:filelist:resource:action:updateOnlineMedia"}(FileListActionEvent||(FileListActionEvent={}));export var FileListActionSelector;!function(e){e.elementSelector="[data-filelist-element]",e.actionSelector="[data-filelist-action]"}(FileListActionSelector||(FileListActionSelector={}));export class FileListActionUtility{static createResourceFromContextDataset(e){return{type:e.filecontextType,identifier:e.filecontextIdentifier,name:e.filecontextName,thumbnail:null,uid:e.filecontextUid?parseInt(e.filecontextUid,10):null,metaUid:e.filecontextMetaUid?parseInt(e.filecontextMetaUid,10):null}}static getResourceForElement(e){return{type:e.dataset.filelistType,identifier:e.dataset.filelistIdentifier,name:e.dataset.filelistName,thumbnail:"filelistThumbnail"in e.dataset&&""!==e.dataset.filelistThumbnail.trim()?e.dataset.filelistThumbnail:null,uid:e.dataset.filelistUid?parseInt(e.dataset.filelistUid,10):null,metaUid:e.dataset.filelistMetaUid?parseInt(e.dataset.filelistMetaUid,10):null}}}class FileListActions{constructor(){new RegularEvent("contextmenu",((e,t)=>{e.preventDefault(),e.stopImmediatePropagation();const i=this.getActionDetail(e,t);if("primary"===i.action)document.dispatchEvent(new CustomEvent(FileListActionEvent.primaryContextmenu,{detail:i}))})).delegateTo(document,FileListActionSelector.actionSelector),new RegularEvent("click",((e,t)=>{e.preventDefault();const i=this.getActionDetail(e,t);switch(i.action){case"primary":document.dispatchEvent(new CustomEvent(FileListActionEvent.primary,{detail:i}));break;case"show":document.dispatchEvent(new CustomEvent(FileListActionEvent.show,{detail:i}));break;case"select":document.dispatchEvent(new CustomEvent(FileListActionEvent.select,{detail:i}));break;case"rename":document.dispatchEvent(new CustomEvent(FileListActionEvent.rename,{detail:i}));break;case"download":document.dispatchEvent(new CustomEvent(FileListActionEvent.download,{detail:i}));break;case"updateOnlineMedia":document.dispatchEvent(new CustomEvent(FileListActionEvent.updateOnlineMedia,{detail:i}))}})).delegateTo(document,FileListActionSelector.actionSelector)}getActionDetail(e,t){const i=t.dataset.filelistAction,n=t.closest(FileListActionSelector.elementSelector);return{event:e,trigger:t,action:i,resources:[FileListActionUtility.getResourceForElement(n)],url:t.dataset.filelistActionUrl??null}}}export default new FileListActions; \ No newline at end of file