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 { 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) => {
<p className="label">
{item.label}
</p>
{item.description && <p className="description">
{item.description}
</p>}
{createIconRender(item.icon)}
{
item.description && <p className="description">
{item.description}
</p>
}
{
createIconRender(item.icon)
}
</div>
})
}
return <div
className="contextMenu"
style={{
top: cords.y,
left: cords.x,
}}
>
{renderItems()}
</div>
return <AnimatePresence>
{
visible && <div
className="context-menu-wrapper"
style={{
top: cords.y,
left: cords.x,
}}
>
<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";
.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;

View File

@ -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)
})
}
}

View File

@ -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 = <DefaultWindow>
{fragment}
</DefaultWindow>
}
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
}
}