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 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
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user