Files
ComfyUI_frontend/src/LiteGraphGlobal.ts
Christian Byrne 3ba61c7265 Don't log every single node being replaced when node defs refreshed (#918)
Currently, when node defs are refreshed (e.g., by pressing "r"
shortcut), every single node def is logged, which is useless when
there's hundred of nodes and clutters console. This PR changes to only
log those messages when in debug mode.
2025-04-11 09:11:25 -07:00

841 lines
26 KiB
TypeScript

import type { Dictionary, ISlotType, Rect, WhenNullish } from "./interfaces"
import { InputIndicators } from "./canvas/InputIndicators"
import { ContextMenu } from "./ContextMenu"
import { CurveEditor } from "./CurveEditor"
import { DragAndScale } from "./DragAndScale"
import { LabelPosition, SlotDirection, SlotShape, SlotType } from "./draw"
import { LGraph } from "./LGraph"
import { LGraphCanvas } from "./LGraphCanvas"
import { LGraphGroup } from "./LGraphGroup"
import { LGraphNode } from "./LGraphNode"
import { LLink } from "./LLink"
import { distance, isInsideRectangle, overlapBounding } from "./measure"
import { Reroute } from "./Reroute"
import {
LGraphEventMode,
LinkDirection,
LinkRenderType,
NodeSlotType,
RenderShape,
TitleMode,
} from "./types/globalEnums"
import { createUuidv4 } from "./utils/uuid"
/**
* The Global Scope. It contains all the registered node classes.
*/
export class LiteGraphGlobal {
// Enums
SlotShape = SlotShape
SlotDirection = SlotDirection
SlotType = SlotType
LabelPosition = LabelPosition
/** Used in serialised graphs at one point. */
VERSION = 0.4 as const
CANVAS_GRID_SIZE = 10
NODE_TITLE_HEIGHT = 30
NODE_TITLE_TEXT_Y = 20
NODE_SLOT_HEIGHT = 20
NODE_WIDGET_HEIGHT = 20
NODE_WIDTH = 140
NODE_MIN_WIDTH = 50
NODE_COLLAPSED_RADIUS = 10
NODE_COLLAPSED_WIDTH = 80
NODE_TITLE_COLOR = "#999"
NODE_SELECTED_TITLE_COLOR = "#FFF"
NODE_TEXT_SIZE = 14
NODE_TEXT_COLOR = "#AAA"
NODE_TEXT_HIGHLIGHT_COLOR = "#EEE"
NODE_SUBTEXT_SIZE = 12
NODE_DEFAULT_COLOR = "#333"
NODE_DEFAULT_BGCOLOR = "#353535"
NODE_DEFAULT_BOXCOLOR = "#666"
NODE_DEFAULT_SHAPE = RenderShape.ROUND
NODE_BOX_OUTLINE_COLOR = "#FFF"
NODE_ERROR_COLOUR = "#E00"
DEFAULT_SHADOW_COLOR = "rgba(0,0,0,0.5)"
DEFAULT_GROUP_FONT = 24
DEFAULT_GROUP_FONT_SIZE?: any
WIDGET_BGCOLOR = "#222"
WIDGET_OUTLINE_COLOR = "#666"
WIDGET_ADVANCED_OUTLINE_COLOR = "rgba(56, 139, 253, 0.8)"
WIDGET_TEXT_COLOR = "#DDD"
WIDGET_SECONDARY_TEXT_COLOR = "#999"
LINK_COLOR = "#9A9"
EVENT_LINK_COLOR = "#A86"
CONNECTING_LINK_COLOR = "#AFA"
/** avoid infinite loops */
MAX_NUMBER_OF_NODES = 10_000
/** default node position */
DEFAULT_POSITION = [100, 100]
/** ,"circle" */
VALID_SHAPES = ["default", "box", "round", "card"] satisfies ("default" | Lowercase<keyof typeof RenderShape>)[]
ROUND_RADIUS = 8
// shapes are used for nodes but also for slots
BOX_SHAPE = RenderShape.BOX
ROUND_SHAPE = RenderShape.ROUND
CIRCLE_SHAPE = RenderShape.CIRCLE
CARD_SHAPE = RenderShape.CARD
ARROW_SHAPE = RenderShape.ARROW
/** intended for slot arrays */
GRID_SHAPE = RenderShape.GRID
// enums
INPUT = NodeSlotType.INPUT
OUTPUT = NodeSlotType.OUTPUT
// TODO: -1 can lead to ambiguity in JS; these should be updated to a more explicit constant or Symbol.
/** for outputs */
EVENT = -1 as const
/** for inputs */
ACTION = -1 as const
/** helper, will add "On Request" and more in the future */
NODE_MODES = ["Always", "On Event", "Never", "On Trigger"]
/** use with node_box_coloured_by_mode */
NODE_MODES_COLORS = ["#666", "#422", "#333", "#224", "#626"]
ALWAYS = LGraphEventMode.ALWAYS
ON_EVENT = LGraphEventMode.ON_EVENT
NEVER = LGraphEventMode.NEVER
ON_TRIGGER = LGraphEventMode.ON_TRIGGER
UP = LinkDirection.UP
DOWN = LinkDirection.DOWN
LEFT = LinkDirection.LEFT
RIGHT = LinkDirection.RIGHT
CENTER = LinkDirection.CENTER
/** helper */
LINK_RENDER_MODES = ["Straight", "Linear", "Spline"]
HIDDEN_LINK = LinkRenderType.HIDDEN_LINK
STRAIGHT_LINK = LinkRenderType.STRAIGHT_LINK
LINEAR_LINK = LinkRenderType.LINEAR_LINK
SPLINE_LINK = LinkRenderType.SPLINE_LINK
NORMAL_TITLE = TitleMode.NORMAL_TITLE
NO_TITLE = TitleMode.NO_TITLE
TRANSPARENT_TITLE = TitleMode.TRANSPARENT_TITLE
AUTOHIDE_TITLE = TitleMode.AUTOHIDE_TITLE
/** arrange nodes vertically */
VERTICAL_LAYOUT = "vertical"
/** used to redirect calls */
proxy = null
node_images_path = ""
debug = false
catch_exceptions = true
throw_errors = true
/** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */
allow_scripts = false
/** nodetypes by string */
registered_node_types: Record<string, typeof LGraphNode> = {}
/** @deprecated used for dropping files in the canvas. It appears the code that enables this was removed, but the object remains and is references by built-in drag drop. */
node_types_by_file_extension: Record<string, { type: string }> = {}
/** node types by classname */
Nodes: Record<string, typeof LGraphNode> = {}
/** used to store vars between graphs */
Globals = {}
/** @deprecated Unused and will be deleted. */
searchbox_extras: Dictionary<unknown> = {}
/** [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback */
node_box_coloured_when_on = false
/** [true!] nodebox based on node mode, visual feedback */
node_box_coloured_by_mode = false
/** [false on mobile] better true if not touch device, TODO add an helper/listener to close if false */
dialog_close_on_mouse_leave = false
dialog_close_on_mouse_leave_delay = 500
/** [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys */
shift_click_do_break_link_from = false
/** [false!]prefer false, way too easy to break links */
click_do_break_link_to = false
/** [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who! */
ctrl_alt_click_do_break_link = true
/** [true!] snaps links when dragging connections over valid targets */
snaps_for_comfy = true
/** [true!] renders a partial border to highlight when a dragged link is snapped to a node */
snap_highlights_node = true
/**
* If `true`, items always snap to the grid - modifier keys are ignored.
* When {@link snapToGrid} is falsy, a value of `1` is used.
* Default: `false`
*/
alwaysSnapToGrid?: boolean
/**
* When set to a positive number, when nodes are moved their positions will
* be rounded to the nearest multiple of this value. Half up.
* Default: `undefined`
* @todo Not implemented - see {@link LiteGraph.CANVAS_GRID_SIZE}
*/
snapToGrid?: number
/** [false on mobile] better true if not touch device, TODO add an helper/listener to close if false */
search_hide_on_mouse_leave = true
/**
* [true!] enable filtering slots type in the search widget
* !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out]
*/
search_filter_enabled = false
/** [true!] opens the results list when opening the search widget */
search_show_all_on_open = true
/**
* [if want false, use true, run, get vars values to be statically set, than disable]
* nodes types and nodeclass association with node types need to be calculated,
* if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out]
*/
auto_load_slot_types = false
// set these values if not using auto_load_slot_types
/** slot types for nodeclass */
registered_slot_in_types: Record<string, { nodes: string[] }> = {}
/** slot types for nodeclass */
registered_slot_out_types: Record<string, { nodes: string[] }> = {}
/** slot types IN */
slot_types_in: string[] = []
/** slot types OUT */
slot_types_out: string[] = []
/**
* specify for each IN slot type a(/many) default node(s), use single string, array, or object
* (with node, title, parameters, ..) like for search
*/
slot_types_default_in: Record<string, string[]> = {}
/**
* specify for each OUT slot type a(/many) default node(s), use single string, array, or object
* (with node, title, parameters, ..) like for search
*/
slot_types_default_out: Record<string, string[]> = {}
/** [true!] very handy, ALT click to clone and drag the new node */
alt_drag_do_clone_nodes = false
/**
* [true!] will create and connect event slots when using action/events connections,
* !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this
*/
do_add_triggers_slots = false
/** [false!] being events, it is strongly reccomended to use them sequentially, one by one */
allow_multi_output_for_events = true
/** [true!] allows to create and connect a ndoe clicking with the third button (wheel) */
middle_click_slot_add_default_node = false
/** [true!] dragging a link to empty space will open a menu, add from list, search or defaults */
release_link_on_empty_shows_menu = false
/** "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) */
pointerevents_method = "pointer"
/**
* [true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected
* with the inputs of the newly pasted nodes
*/
ctrl_shift_v_paste_connect_unselected_outputs = true
// if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers.
// use this if you must have node IDs that are unique across all graphs and subgraphs.
use_uuids = false
// Whether to highlight the bounding box of selected groups
highlight_selected_group = true
/** Whether to scale context with the graph when zooming in. Zooming out never makes context menus smaller. */
context_menu_scaling = false
// TODO: Remove legacy accessors
LGraph = LGraph
LLink = LLink
LGraphNode = LGraphNode
LGraphGroup = LGraphGroup
DragAndScale = DragAndScale
LGraphCanvas = LGraphCanvas
ContextMenu = ContextMenu
CurveEditor = CurveEditor
Reroute = Reroute
InputIndicators = InputIndicators
onNodeTypeRegistered?(type: string, base_class: typeof LGraphNode): void
onNodeTypeReplaced?(type: string, base_class: typeof LGraphNode, prev: unknown): void
/**
* Register a node class so it can be listed when the user wants to create a new one
* @param type name of the node and path
* @param base_class class containing the structure of a node
*/
registerNodeType(type: string, base_class: typeof LGraphNode): void {
if (!base_class.prototype)
throw "Cannot register a simple object, it must be a class with a prototype"
base_class.type = type
if (this.debug) console.log("Node registered:", type)
const classname = base_class.name
const pos = type.lastIndexOf("/")
base_class.category = type.substring(0, pos)
base_class.title ||= classname
// extend class
for (const i in LGraphNode.prototype) {
// @ts-expect-error #576 This functionality is deprecated and should be removed.
base_class.prototype[i] ||= LGraphNode.prototype[i]
}
const prev = this.registered_node_types[type]
if (prev && this.debug) {
console.log("replacing node type:", type)
}
this.registered_node_types[type] = base_class
if (base_class.constructor.name) this.Nodes[classname] = base_class
this.onNodeTypeRegistered?.(type, base_class)
if (prev) this.onNodeTypeReplaced?.(type, base_class, prev)
// warnings
if (base_class.prototype.onPropertyChange)
console.warn(`LiteGraph node class ${type} has onPropertyChange method, it must be called onPropertyChanged with d at the end`)
// TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
if (this.auto_load_slot_types) new base_class(base_class.title || "tmpnode")
}
/**
* removes a node type from the system
* @param type name of the node or the node constructor itself
*/
unregisterNodeType(type: string | typeof LGraphNode): void {
const base_class = typeof type === "string"
? this.registered_node_types[type]
: type
if (!base_class) throw `node type not found: ${String(type)}`
delete this.registered_node_types[String(base_class.type)]
const name = base_class.constructor.name
if (name) delete this.Nodes[name]
}
/**
* Save a slot type and his node
* @param type name of the node or the node constructor itself
* @param slot_type name of the slot type (variable type), eg. string, number, array, boolean, ..
*/
registerNodeAndSlotType(
type: LGraphNode,
slot_type: ISlotType,
out?: boolean,
): void {
out ||= false
// @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor.
const base_class = typeof type === "string" && this.registered_node_types[type] !== "anonymous"
? this.registered_node_types[type]
: type
// @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor.
const class_type = base_class.constructor.type
let allTypes = []
if (typeof slot_type === "string") {
allTypes = slot_type.split(",")
} else if (slot_type == this.EVENT || slot_type == this.ACTION) {
allTypes = ["_event_"]
} else {
allTypes = ["*"]
}
for (let slotType of allTypes) {
if (slotType === "") slotType = "*"
const registerTo = out
? "registered_slot_out_types"
: "registered_slot_in_types"
if (this[registerTo][slotType] === undefined)
this[registerTo][slotType] = { nodes: [] }
if (!this[registerTo][slotType].nodes.includes(class_type))
this[registerTo][slotType].nodes.push(class_type)
// check if is a new type
const types = out
? this.slot_types_out
: this.slot_types_in
if (!types.includes(slotType.toLowerCase())) {
types.push(slotType.toLowerCase())
types.sort()
}
}
}
/**
* Removes all previously registered node's types
*/
clearRegisteredTypes(): void {
this.registered_node_types = {}
this.node_types_by_file_extension = {}
this.Nodes = {}
this.searchbox_extras = {}
}
/**
* Create a node of a given type with a name. The node is not attached to any graph yet.
* @param type full name of the node class. p.e. "math/sin"
* @param title a name to distinguish from other nodes
* @param options to set options
*/
createNode(
type: string,
title?: string,
options?: Dictionary<unknown>,
): LGraphNode | null {
const base_class = this.registered_node_types[type]
if (!base_class) {
if (this.debug) console.log(`GraphNode type "${type}" not registered.`)
return null
}
title = title || base_class.title || type
let node = null
if (this.catch_exceptions) {
try {
node = new base_class(title)
} catch (error) {
console.error(error)
return null
}
} else {
node = new base_class(title)
}
node.type = type
if (!node.title && title) node.title = title
node.properties ||= {}
node.properties_info ||= []
node.flags ||= {}
// call onresize?
node.size ||= node.computeSize()
node.pos ||= [this.DEFAULT_POSITION[0], this.DEFAULT_POSITION[1]]
node.mode ||= LGraphEventMode.ALWAYS
// extra options
if (options) {
for (const i in options) {
// @ts-expect-error #577 Requires interface
node[i] = options[i]
}
}
// callback
node.onNodeCreated?.()
return node
}
/**
* Returns a registered node type with a given name
* @param type full name of the node class. p.e. "math/sin"
* @returns the node class
*/
getNodeType(type: string): typeof LGraphNode {
return this.registered_node_types[type]
}
/**
* Returns a list of node types matching one category
* @param category category name
* @returns array with all the node classes
*/
getNodeTypesInCategory(category: string, filter?: string) {
const r = []
for (const i in this.registered_node_types) {
const type = this.registered_node_types[i]
if (type.filter != filter) continue
if (category == "") {
if (type.category == null) r.push(type)
} else if (type.category == category) {
r.push(type)
}
}
return r
}
/**
* Returns a list with all the node type categories
* @param filter only nodes with ctor.filter equal can be shown
* @returns array with all the names of the categories
*/
getNodeTypesCategories(filter?: string): string[] {
const categories: Dictionary<number> = { "": 1 }
for (const i in this.registered_node_types) {
const type = this.registered_node_types[i]
if (type.category && !type.skip_list) {
if (type.filter != filter) continue
categories[type.category] = 1
}
}
const result = []
for (const i in categories) {
result.push(i)
}
return result
}
// debug purposes: reloads all the js scripts that matches a wildcard
reloadNodes(folder_wildcard: string): void {
const tmp = document.getElementsByTagName("script")
// weird, this array changes by its own, so we use a copy
const script_files = []
for (const element of tmp) {
script_files.push(element)
}
const docHeadObj = document.getElementsByTagName("head")[0]
folder_wildcard = document.location.href + folder_wildcard
for (const script_file of script_files) {
const src = script_file.src
if (!src || src.substr(0, folder_wildcard.length) != folder_wildcard)
continue
try {
if (this.debug) console.log("Reloading:", src)
const dynamicScript = document.createElement("script")
dynamicScript.type = "text/javascript"
dynamicScript.src = src
docHeadObj.append(dynamicScript)
script_file.remove()
} catch (error) {
if (this.throw_errors) throw error
if (this.debug) console.log("Error while reloading", src)
}
}
if (this.debug) console.log("Nodes reloaded")
}
// separated just to improve if it doesn't work
/** @deprecated Prefer {@link structuredClone} */
cloneObject<T extends object | undefined | null>(obj: T, target?: T): WhenNullish<T, null> {
if (obj == null) return null as WhenNullish<T, null>
const r = JSON.parse(JSON.stringify(obj))
if (!target) return r
for (const i in r) {
// @ts-expect-error deprecated
target[i] = r[i]
}
return target
}
/** @see {@link createUuidv4} @inheritdoc */
uuidv4 = createUuidv4
/**
* Returns if the types of two slots are compatible (taking into account wildcards, etc)
* @param type_a output
* @param type_b input
* @returns true if they can be connected
*/
isValidConnection(type_a: ISlotType, type_b: ISlotType): boolean {
if (type_a == "" || type_a === "*") type_a = 0
if (type_b == "" || type_b === "*") type_b = 0
// If generic in/output, matching types (valid for triggers), or event/action types
if (
!type_a ||
!type_b ||
type_a == type_b ||
(type_a == this.EVENT && type_b == this.ACTION)
) {
return true
}
// Enforce string type to handle toLowerCase call (-1 number not ok)
type_a = String(type_a)
type_b = String(type_b)
type_a = type_a.toLowerCase()
type_b = type_b.toLowerCase()
// For nodes supporting multiple connection types
if (!type_a.includes(",") && !type_b.includes(","))
return type_a == type_b
// Check all permutations to see if one is valid
const supported_types_a = type_a.split(",")
const supported_types_b = type_b.split(",")
for (const a of supported_types_a) {
for (const b of supported_types_b) {
if (this.isValidConnection(a, b))
return true
}
}
return false
}
// used to create nodes from wrapping functions
getParameterNames(func: (...args: any) => any): string[] {
return String(func)
.replaceAll(/\/\/.*$/gm, "") // strip single-line comments
.replaceAll(/\s+/g, "") // strip white space
.replaceAll(/\/\*[^*/]*\*\//g, "") // strip multi-line comments /**/
.split("){", 1)[0]
.replace(/^[^(]*\(/, "") // extract the parameters
.replaceAll(/=[^,]+/g, "") // strip any ES6 defaults
.split(",")
.filter(Boolean) // split & filter [""]
}
/* helper for interaction: pointer, touch, mouse Listeners
used by LGraphCanvas DragAndScale ContextMenu */
pointerListenerAdd(oDOM: Node, sEvIn: string, fCall: (e: Event) => boolean | void, capture = false): void {
if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall !== "function") return
let sMethod = this.pointerevents_method
let sEvent = sEvIn
// UNDER CONSTRUCTION
// convert pointerevents to touch event when not available
if (sMethod == "pointer" && !window.PointerEvent) {
console.warn("sMethod=='pointer' && !window.PointerEvent")
console.log(`Converting pointer[${sEvent}] : down move up cancel enter TO touchstart touchmove touchend, etc ..`)
switch (sEvent) {
case "down": {
sMethod = "touch"
sEvent = "start"
break
}
case "move": {
sMethod = "touch"
// sEvent = "move";
break
}
case "up": {
sMethod = "touch"
sEvent = "end"
break
}
case "cancel": {
sMethod = "touch"
// sEvent = "cancel";
break
}
case "enter": {
console.log("debug: Should I send a move event?") // ???
break
}
// case "over": case "out": not used at now
default: {
console.warn(`PointerEvent not available in this browser ? The event ${sEvent} would not be called`)
}
}
}
switch (sEvent) {
// @ts-expect-error
// both pointer and move events
case "down": case "up": case "move": case "over": case "out": case "enter":
{
oDOM.addEventListener(sMethod + sEvent, fCall, capture)
}
// @ts-expect-error
// only pointerevents
case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture":
{
if (sMethod != "mouse") {
return oDOM.addEventListener(sMethod + sEvent, fCall, capture)
}
}
// not "pointer" || "mouse"
default:
return oDOM.addEventListener(sEvent, fCall, capture)
}
}
pointerListenerRemove(oDOM: Node, sEvent: string, fCall: (e: Event) => boolean | void, capture = false): void {
if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall !== "function") return
switch (sEvent) {
// @ts-expect-error
// both pointer and move events
case "down": case "up": case "move": case "over": case "out": case "enter":
{
if (this.pointerevents_method == "pointer" || this.pointerevents_method == "mouse") {
oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture)
}
}
// @ts-expect-error
// only pointerevents
case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture":
{
if (this.pointerevents_method == "pointer") {
return oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture)
}
}
// not "pointer" || "mouse"
default:
return oDOM.removeEventListener(sEvent, fCall, capture)
}
}
getTime(): number {
return performance.now()
}
distance = distance
colorToString(c: [number, number, number, number]): string {
return (
`rgba(${
Math.round(c[0] * 255).toFixed()
},${
Math.round(c[1] * 255).toFixed()
},${
Math.round(c[2] * 255).toFixed()
},${
c.length == 4 ? c[3].toFixed(2) : "1.0"
})`
)
}
isInsideRectangle = isInsideRectangle
// [minx,miny,maxx,maxy]
growBounding(bounding: Rect, x: number, y: number): void {
if (x < bounding[0]) {
bounding[0] = x
} else if (x > bounding[2]) {
bounding[2] = x
}
if (y < bounding[1]) {
bounding[1] = y
} else if (y > bounding[3]) {
bounding[3] = y
}
}
overlapBounding = overlapBounding
// point inside bounding box
isInsideBounding(p: number[], bb: number[][]): boolean {
if (
p[0] < bb[0][0] ||
p[1] < bb[0][1] ||
p[0] > bb[1][0] ||
p[1] > bb[1][1]
) {
return false
}
return true
}
// Convert a hex value to its decimal value - the inputted hex must be in the
// format of a hex triplet - the kind we use for HTML colours. The function
// will return an array with three values.
hex2num(hex: string): number[] {
if (hex.charAt(0) == "#") {
hex = hex.slice(1)
// Remove the '#' char - if there is one.
}
hex = hex.toUpperCase()
const hex_alphabets = "0123456789ABCDEF"
const value = new Array(3)
let k = 0
let int1, int2
for (let i = 0; i < 6; i += 2) {
int1 = hex_alphabets.indexOf(hex.charAt(i))
int2 = hex_alphabets.indexOf(hex.charAt(i + 1))
value[k] = int1 * 16 + int2
k++
}
return value
}
// Give a array with three values as the argument and the function will return
// the corresponding hex triplet.
num2hex(triplet: number[]): string {
const hex_alphabets = "0123456789ABCDEF"
let hex = "#"
let int1, int2
for (let i = 0; i < 3; i++) {
int1 = triplet[i] / 16
int2 = triplet[i] % 16
hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2)
}
return hex
}
closeAllContextMenus(ref_window: Window = window): void {
const elements = [...ref_window.document.querySelectorAll(".litecontextmenu")]
if (!elements.length) return
for (const element of elements) {
if ("close" in element && typeof element.close === "function") {
element.close()
} else {
element.remove()
}
}
}
extendClass(target: any, origin: any): void {
for (const i in origin) {
// copy class properties
if (target.hasOwnProperty(i)) continue
target[i] = origin[i]
}
if (origin.prototype) {
// copy prototype properties
for (const i in origin.prototype) {
// only enumerable
if (!origin.prototype.hasOwnProperty(i)) continue
// avoid overwriting existing ones
if (target.prototype.hasOwnProperty(i)) continue
// copy getters
if (origin.prototype.__lookupGetter__(i)) {
target.prototype.__defineGetter__(
i,
origin.prototype.__lookupGetter__(i),
)
} else {
target.prototype[i] = origin.prototype[i]
}
// and setters
if (origin.prototype.__lookupSetter__(i)) {
target.prototype.__defineSetter__(
i,
origin.prototype.__lookupSetter__(i),
)
}
}
}
}
}