Spaces:
Running
Running
// This file is imported from | |
// https://github.com/ScratchAddons/ScratchAddons/blob/master/addon-api/content-script/contextmenu.js | |
/* eslint-disable */ | |
let initialized = false; | |
let hasDynamicContextMenu = false; | |
let contextMenus = []; | |
const onReactContextMenu = function (e) { | |
if (!e.target) return; | |
const ctxTarget = e.target.closest(".react-contextmenu-wrapper"); | |
if (!ctxTarget) return; | |
let ctxMenu = ctxTarget.querySelector("nav.react-contextmenu"); | |
let type; | |
const extra = {}; | |
if (false && !ctxMenu && ctxTarget.closest(".monitor-overlay")) { | |
// Monitors are rendered on document.body. | |
// This is internal id which is different from the actual monitor ID. | |
// Optional chain just to prevent crashes when they change the internal stuff. | |
const mInternalId = ctxTarget[this.traps.getInternalKey(ctxTarget)]?.return?.stateNode?.props?.id; | |
if (!mInternalId) return; | |
ctxMenu = Array.prototype.find.call( | |
document.querySelectorAll("body > nav.react-contextmenu"), | |
(candidate) => candidate[this.traps.getInternalKey(candidate)]?.return?.stateNode?.props?.id === mInternalId | |
); | |
if (!ctxMenu) return; | |
const props = ctxTarget[this.traps.getInternalKey(ctxTarget)]?.return?.return?.return?.stateNode?.props; | |
if (!props) return; | |
extra.monitorParams = props.params; | |
extra.opcode = props.opcode; | |
extra.itemId = props.id; | |
extra.targetId = props.targetId; | |
type = `monitor_${props.mode}`; | |
} else if (ctxTarget[this.traps.getInternalKey(ctxTarget)]?.return?.return?.return?.stateNode?.props?.dragType) { | |
// SpriteSelectorItem which despite its name is used for costumes, sounds, backpacked script etc | |
const props = ctxTarget[this.traps.getInternalKey(ctxTarget)].return.return.return.stateNode.props; | |
type = props.dragType.toLowerCase(); | |
extra.name = props.name; | |
extra.itemId = props.id; | |
extra.index = props.index; | |
} else { | |
return; | |
} | |
const ctx = { | |
menuItem: ctxMenu, | |
target: ctxTarget, | |
type, | |
...extra, | |
}; | |
Array.from(ctxMenu.children).forEach((existing) => { | |
if (existing.classList.contains("sa-ctx-menu")) existing.remove(); | |
}); | |
for (const item of hasDynamicContextMenu | |
? contextMenus.flatMap((menu) => (typeof menu === "function" ? menu(type, ctx) : menu)) | |
: contextMenus) { | |
if (!item) continue; | |
if (item.types && !item.types.some((itemType) => type === itemType)) continue; | |
if (item.condition && !item.condition(ctx)) continue; | |
const itemElem = document.createElement("div"); | |
const classes = ["context-menu_menu-item"]; | |
if (item.border) classes.push("context-menu_menu-item-bordered"); | |
if (item.dangerous) classes.push("context-menu_menu-item-danger"); | |
itemElem.className = this.scratchClass(...classes, { | |
others: ["react-contextmenu-item", "sa-ctx-menu", item.className || ""], | |
}); | |
const label = document.createElement("span"); | |
label.textContent = item.label; | |
itemElem.append(label); | |
this.displayNoneWhileDisabled(itemElem, { | |
display: "block", | |
}); | |
itemElem.addEventListener("click", (e) => { | |
e.stopPropagation(); | |
window.dispatchEvent( | |
new CustomEvent("REACT_CONTEXTMENU_HIDE", { | |
detail: { | |
action: "REACT_CONTEXTMENU_HIDE", | |
}, | |
}) | |
); | |
item.callback(ctx); | |
}); | |
this.appendToSharedSpace({ | |
space: item.position, | |
order: item.order, | |
scope: ctxMenu, | |
element: itemElem, | |
}); | |
} | |
return; | |
}; | |
const initialize = (tab) => { | |
if (initialized) return; | |
initialized = true; | |
document.body.addEventListener("contextmenu", (e) => onReactContextMenu.call(tab, e), { capture: true }); | |
}; | |
export const addContextMenu = (tab, callback, opts) => { | |
if (typeof opts === "undefined") { | |
contextMenus.push(callback); | |
hasDynamicContextMenu = true; | |
} else { | |
contextMenus.push({ | |
...opts, | |
callback, | |
}); | |
} | |
initialize(tab); | |
}; | |