improve win mng & context menu animations

This commit is contained in:
SrGooglo 2024-09-12 17:50:06 +00:00
parent dcf7124f20
commit 0ce6d39a59
4 changed files with 149 additions and 81 deletions

View File

@ -1,12 +1,23 @@
import React from "react" import React from "react"
import { createIconRender } from "@components/Icons" import { createIconRender } from "@components/Icons"
import { AnimatePresence, motion } from "framer-motion"
import "./index.less" import "./index.less"
export default (props) => { const ContextMenu = (props) => {
const [visible, setVisible] = React.useState(true)
const { items = [], cords, clickedComponent, ctx } = props const { items = [], cords, clickedComponent, ctx } = props
async function onClose() {
setVisible(false)
props.unregisterOnClose(onClose)
}
React.useEffect(() => {
props.registerOnClose(onClose)
}, [])
const handleItemClick = async (item) => { const handleItemClick = async (item) => {
if (typeof item.action === "function") { if (typeof item.action === "function") {
await item.action(clickedComponent, ctx) await item.action(clickedComponent, ctx)
@ -33,21 +44,43 @@ export default (props) => {
<p className="label"> <p className="label">
{item.label} {item.label}
</p> </p>
{item.description && <p className="description">
{item.description} {
</p>} item.description && <p className="description">
{createIconRender(item.icon)} {item.description}
</p>
}
{
createIconRender(item.icon)
}
</div> </div>
}) })
} }
return <div return <AnimatePresence>
className="contextMenu" {
style={{ visible && <div
top: cords.y, className="context-menu-wrapper"
left: cords.x, style={{
}} top: cords.y,
> left: cords.x,
{renderItems()} }}
</div> >
} <motion.div
className="context-menu"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.3 }}
transition={{ duration: 0.05, ease: "easeInOut" }}
>
{
renderItems()
}
</motion.div>
</div>
}
</AnimatePresence>
}
export default ContextMenu

View File

@ -1,15 +1,21 @@
@import "@styles/vars.less"; @import "@styles/vars.less";
.contextMenu { .context-menu-wrapper {
position: fixed; position: fixed;
z-index: 100000; z-index: 100000;
display: flex;
flex-direction: column;
top: 0; top: 0;
left: 0; left: 0;
transition: all 150ms ease-in-out;
}
.context-menu {
position: relative;
display: flex;
flex-direction: column;
width: 230px; width: 230px;
height: fit-content; height: fit-content;

View File

@ -1,5 +1,6 @@
import React from "react" import React from "react"
import Core from "evite/src/core" import Core from "evite/src/core"
import EventEmitter from "evite/src/internals/EventEmitter"
import ContextMenu from "./components/contextMenu" import ContextMenu from "./components/contextMenu"
@ -9,17 +10,13 @@ import PostCardContext from "@config/context-menu/post"
export default class ContextMenuCore extends Core { export default class ContextMenuCore extends Core {
static namespace = "contextMenu" static namespace = "contextMenu"
public = {
show: this.show.bind(this),
hide: this.hide.bind(this),
registerContext: this.registerContext.bind(this),
}
contexts = { contexts = {
...DefaultContenxt, ...DefaultContenxt,
...PostCardContext, ...PostCardContext,
} }
eventBus = new EventEmitter()
async onInitialize() { async onInitialize() {
if (app.isMobile) { if (app.isMobile) {
this.console.warn("Context menu is not available on mobile") 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)) 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 this.contexts[element] = context
} }
async generateItems(element) { generateItems = async (element) => {
let items = [] let items = []
// find the closest context with attribute (context-menu) // find the closest context with attribute (context-menu)
@ -72,7 +106,7 @@ export default class ContextMenuCore extends Core {
if (typeof contextObject === "function") { if (typeof contextObject === "function") {
contextObject = await contextObject(items, parentElement, element, { contextObject = await contextObject(items, parentElement, element, {
close: this.hide, close: this.onClose,
}) })
} }
@ -102,49 +136,23 @@ export default class ContextMenuCore extends Core {
return items return items
} }
async handleEvent(event) { show = async (payload) => {
event.preventDefault() app.cores.window_mng.render(
"context-menu-portal",
// get the cords of the mouse React.createElement(ContextMenu, payload),
const x = event.clientX {
const y = event.clientY onClose: this.onClose,
createOrUpdate: true,
// get the component that was clicked closeOnClickOutside: true,
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,
}, },
clickedComponent: component, )
items: items,
ctx: {
close: this.hide.bind(this),
}
})
} }
show(payload) { onClose = async (delay = 200) => {
app.cores.window_mng.render("context-menu", React.createElement(ContextMenu, payload), { this.eventBus.emit("close", delay)
createOrUpdate: true,
closeOnClickOutside: true,
})
}
hide() { await new Promise((resolve) => {
app.cores.window_mng.close("context-menu") setTimeout(resolve, delay)
})
} }
} }

View File

@ -5,8 +5,6 @@ import { createRoot } from "react-dom/client"
import DefaultWindow from "./components/defaultWindow" import DefaultWindow from "./components/defaultWindow"
import DefaultWindowContext from "./components/defaultWindow/context"
import "./index.less" import "./index.less"
export default class WindowManager extends Core { export default class WindowManager extends Core {
@ -15,7 +13,6 @@ export default class WindowManager extends Core {
static idMount = "windows" static idMount = "windows"
root = null root = null
windows = [] windows = []
public = { 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. * @param {boolean} options.createOrUpdate - Specifies whether to create a new element or update an existing one.
* @return {HTMLElement} The created or updated element. * @return {HTMLElement} The created or updated element.
*/ */
render( async render(
id, id,
fragment, fragment,
{ {
useFrame = false, useFrame = false,
onClose = null,
createOrUpdate = false, createOrUpdate = false,
closeOnClickOutside = false, closeOnClickOutside = false,
} = {} } = {}
@ -68,6 +66,7 @@ export default class WindowManager extends Core {
let win = null let win = null
// check if window already exist // check if window already exist
// if exist, try to automatically generate a new id
if (this.root.querySelector(`#${id}`) && !createOrUpdate) { if (this.root.querySelector(`#${id}`) && !createOrUpdate) {
const newId = `${id}_${Date.now()}` const newId = `${id}_${Date.now()}`
@ -76,6 +75,17 @@ export default class WindowManager extends Core {
id = newId 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) { if (this.root.querySelector(`#${id}`) && createOrUpdate) {
element = document.getElementById(id) element = document.getElementById(id)
@ -96,24 +106,23 @@ export default class WindowManager extends Core {
win = { win = {
id: id, id: id,
node: node, node: node,
onClose: onClose,
closeOnClickOutside: closeOnClickOutside,
} }
this.windows.push(win) this.windows.push(win)
} }
// if useFrame is true, wrap the fragment with a DefaultWindow component
if (useFrame) { if (useFrame) {
fragment = <DefaultWindow> fragment = <DefaultWindow>
{fragment} {fragment}
</DefaultWindow> </DefaultWindow>
} }
if (closeOnClickOutside) {
document.addEventListener("click", (e) => this.handleWrapperClick(id, e))
}
node.render(React.cloneElement(fragment, { node.render(React.cloneElement(fragment, {
close: () => { 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. * @param {string} id - The ID of the window to be closed.
* @return {boolean} Returns true if the window was successfully closed, false otherwise. * @return {boolean} Returns true if the window was successfully closed, false otherwise.
*/ */
close(id) { async close(id) {
const element = document.getElementById(id) const element = document.getElementById(id)
const win = this.windows.find((node) => { const win = this.windows.find((node) => {
return node.id === id return node.id === id
}) })
if (!win) { if (!win || !element) {
this.console.warn(`Window ${id} not found`) this.console.error(`Window [${id}] not found`)
return false 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() win.node.unmount()
this.root.removeChild(element) 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) => { this.windows = this.windows.filter((node) => {
return node.id !== id return node.id !== id
}) })
this.console.debug(`Window ${id} closed`)
return true return true
} }
} }