mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
improve win mng & context menu animations
This commit is contained in:
parent
dcf7124f20
commit
0ce6d39a59
@ -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
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user