From 0ce6d39a5975aec3fec18857736f720968c40292 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Thu, 12 Sep 2024 17:50:06 +0000 Subject: [PATCH] improve win mng & context menu animations --- .../components/contextMenu/index.jsx | 63 ++++++++--- .../components/contextMenu/index.less | 14 ++- .../cores/contextMenu/context_menu.core.js | 106 ++++++++++-------- .../app/src/cores/windows/windows.core.jsx | 47 +++++--- 4 files changed, 149 insertions(+), 81 deletions(-) diff --git a/packages/app/src/cores/contextMenu/components/contextMenu/index.jsx b/packages/app/src/cores/contextMenu/components/contextMenu/index.jsx index 53fdff78..6c99f413 100755 --- a/packages/app/src/cores/contextMenu/components/contextMenu/index.jsx +++ b/packages/app/src/cores/contextMenu/components/contextMenu/index.jsx @@ -1,12 +1,23 @@ import React from "react" import { createIconRender } from "@components/Icons" +import { AnimatePresence, motion } from "framer-motion" import "./index.less" -export default (props) => { +const ContextMenu = (props) => { + const [visible, setVisible] = React.useState(true) const { items = [], cords, clickedComponent, ctx } = props + async function onClose() { + setVisible(false) + props.unregisterOnClose(onClose) + } + + React.useEffect(() => { + props.registerOnClose(onClose) + }, []) + const handleItemClick = async (item) => { if (typeof item.action === "function") { await item.action(clickedComponent, ctx) @@ -33,21 +44,43 @@ export default (props) => {

{item.label}

- {item.description &&

- {item.description} -

} - {createIconRender(item.icon)} + + { + item.description &&

+ {item.description} +

+ } + + { + createIconRender(item.icon) + } }) } - return
- {renderItems()} -
-} \ No newline at end of file + return + { + visible &&
+ + { + renderItems() + } + +
+ } +
+} + +export default ContextMenu \ No newline at end of file diff --git a/packages/app/src/cores/contextMenu/components/contextMenu/index.less b/packages/app/src/cores/contextMenu/components/contextMenu/index.less index eab8f613..d4c04735 100755 --- a/packages/app/src/cores/contextMenu/components/contextMenu/index.less +++ b/packages/app/src/cores/contextMenu/components/contextMenu/index.less @@ -1,15 +1,21 @@ @import "@styles/vars.less"; -.contextMenu { +.context-menu-wrapper { position: fixed; z-index: 100000; - display: flex; - flex-direction: column; - top: 0; left: 0; + transition: all 150ms ease-in-out; +} + +.context-menu { + position: relative; + + display: flex; + flex-direction: column; + width: 230px; height: fit-content; diff --git a/packages/app/src/cores/contextMenu/context_menu.core.js b/packages/app/src/cores/contextMenu/context_menu.core.js index f618a295..9b160e63 100755 --- a/packages/app/src/cores/contextMenu/context_menu.core.js +++ b/packages/app/src/cores/contextMenu/context_menu.core.js @@ -1,5 +1,6 @@ import React from "react" import Core from "evite/src/core" +import EventEmitter from "evite/src/internals/EventEmitter" import ContextMenu from "./components/contextMenu" @@ -9,17 +10,13 @@ import PostCardContext from "@config/context-menu/post" export default class ContextMenuCore extends Core { static namespace = "contextMenu" - public = { - show: this.show.bind(this), - hide: this.hide.bind(this), - registerContext: this.registerContext.bind(this), - } - contexts = { ...DefaultContenxt, ...PostCardContext, } + eventBus = new EventEmitter() + async onInitialize() { if (app.isMobile) { this.console.warn("Context menu is not available on mobile") @@ -29,11 +26,48 @@ export default class ContextMenuCore extends Core { document.addEventListener("contextmenu", this.handleEvent.bind(this)) } - registerContext(element, context) { + async handleEvent(event) { + event.preventDefault() + + // get the cords of the mouse + const x = event.clientX + const y = event.clientY + + // get the component that was clicked + const component = document.elementFromPoint(x, y) + + // check if is clicking inside a context menu or a children inside a context menu + if (component.classList.contains("contextMenu") || component.closest(".contextMenu")) { + return + } + + const items = await this.generateItems(component) + + if (!items) { + this.console.warn("No context menu items found, aborting") + return false + } + + this.show({ + registerOnClose: (cb) => { this.eventBus.on("close", cb) }, + unregisterOnClose: (cb) => { this.eventBus.off("close", cb) }, + cords: { + x, + y, + }, + clickedComponent: component, + items: items, + ctx: { + close: this.onClose, + } + }) + } + + registerContext = async (element, context) => { this.contexts[element] = context } - async generateItems(element) { + generateItems = async (element) => { let items = [] // find the closest context with attribute (context-menu) @@ -72,7 +106,7 @@ export default class ContextMenuCore extends Core { if (typeof contextObject === "function") { contextObject = await contextObject(items, parentElement, element, { - close: this.hide, + close: this.onClose, }) } @@ -102,49 +136,23 @@ export default class ContextMenuCore extends Core { return items } - async handleEvent(event) { - event.preventDefault() - - // get the cords of the mouse - const x = event.clientX - const y = event.clientY - - // get the component that was clicked - const component = document.elementFromPoint(x, y) - - // check if is clicking inside a context menu or a children inside a context menu - if (component.classList.contains("contextMenu") || component.closest(".contextMenu")) { - return - } - - const items = await this.generateItems(component) - - if (!items) { - this.console.warn("No context menu items found, aborting") - return false - } - - this.show({ - cords: { - x, - y, + show = async (payload) => { + app.cores.window_mng.render( + "context-menu-portal", + React.createElement(ContextMenu, payload), + { + onClose: this.onClose, + createOrUpdate: true, + closeOnClickOutside: true, }, - clickedComponent: component, - items: items, - ctx: { - close: this.hide.bind(this), - } - }) + ) } - show(payload) { - app.cores.window_mng.render("context-menu", React.createElement(ContextMenu, payload), { - createOrUpdate: true, - closeOnClickOutside: true, - }) - } + onClose = async (delay = 200) => { + this.eventBus.emit("close", delay) - hide() { - app.cores.window_mng.close("context-menu") + await new Promise((resolve) => { + setTimeout(resolve, delay) + }) } } \ No newline at end of file diff --git a/packages/app/src/cores/windows/windows.core.jsx b/packages/app/src/cores/windows/windows.core.jsx index 5512a974..b9d2a1f8 100644 --- a/packages/app/src/cores/windows/windows.core.jsx +++ b/packages/app/src/cores/windows/windows.core.jsx @@ -5,8 +5,6 @@ import { createRoot } from "react-dom/client" import DefaultWindow from "./components/defaultWindow" -import DefaultWindowContext from "./components/defaultWindow/context" - import "./index.less" export default class WindowManager extends Core { @@ -15,7 +13,6 @@ export default class WindowManager extends Core { static idMount = "windows" root = null - windows = [] public = { @@ -54,11 +51,12 @@ export default class WindowManager extends Core { * @param {boolean} options.createOrUpdate - Specifies whether to create a new element or update an existing one. * @return {HTMLElement} The created or updated element. */ - render( + async render( id, fragment, { useFrame = false, + onClose = null, createOrUpdate = false, closeOnClickOutside = false, } = {} @@ -68,6 +66,7 @@ export default class WindowManager extends Core { let win = null // check if window already exist + // if exist, try to automatically generate a new id if (this.root.querySelector(`#${id}`) && !createOrUpdate) { const newId = `${id}_${Date.now()}` @@ -76,6 +75,17 @@ export default class WindowManager extends Core { id = newId } + // if closeOnClickOutside is true, add click event listener + if (closeOnClickOutside === true) { + document.addEventListener( + "click", + (e) => this.handleWrapperClick(id, e), + { once: true }, + ) + } + + // check if window already exist, if exist and createOrUpdate is true, update the element + // if not exist, create a new element if (this.root.querySelector(`#${id}`) && createOrUpdate) { element = document.getElementById(id) @@ -96,24 +106,23 @@ export default class WindowManager extends Core { win = { id: id, node: node, + onClose: onClose, + closeOnClickOutside: closeOnClickOutside, } this.windows.push(win) } + // if useFrame is true, wrap the fragment with a DefaultWindow component if (useFrame) { fragment = {fragment} } - if (closeOnClickOutside) { - document.addEventListener("click", (e) => this.handleWrapperClick(id, e)) - } - node.render(React.cloneElement(fragment, { close: () => { - this.close(id) + this.close(id, onClose) } })) @@ -129,26 +138,38 @@ export default class WindowManager extends Core { * @param {string} id - The ID of the window to be closed. * @return {boolean} Returns true if the window was successfully closed, false otherwise. */ - close(id) { + async close(id) { const element = document.getElementById(id) const win = this.windows.find((node) => { return node.id === id }) - if (!win) { - this.console.warn(`Window ${id} not found`) - + if (!win || !element) { + this.console.error(`Window [${id}] not found`) return false } + this.console.debug(`Closing window ${id}`, win, element) + + // if onClose callback is defined, call it + if (typeof win.onClose === "function") { + this.console.debug(`Trigging close callback for window ${id}`) + await win.onClose() + } + + // remove the element from the DOM + this.console.debug(`Removing element from DOM for window ${id}`) win.node.unmount() this.root.removeChild(element) + // remove the window from the list + this.console.debug(`Removing window from list for window ${id}`) this.windows = this.windows.filter((node) => { return node.id !== id }) + this.console.debug(`Window ${id} closed`) return true } } \ No newline at end of file