[TS] Fix ContextMenu types (#649)

- No runtime changes
- Drastically improves ContextMenu type accuracy / safety
- Allows TS strict conversion
This commit is contained in:
filtered
2025-03-01 01:52:07 +11:00
committed by GitHub
parent b877312336
commit c4faaf4210
6 changed files with 92 additions and 82 deletions

View File

@@ -1,28 +1,21 @@
import type { IContextMenuOptions, IContextMenuValue } from "./interfaces"
import type { ContextMenuDivElement, IContextMenuOptions, IContextMenuValue } from "./interfaces"
import { LiteGraph } from "./litegraph"
interface ContextMenuDivElement extends HTMLDivElement {
value?: IContextMenuValue | string
onclick_callback?: never
closing_timer?: number
}
// TODO: Replace this pattern with something more modern.
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export interface ContextMenu {
constructor: new (...args: ConstructorParameters<typeof ContextMenu>) => ContextMenu
export interface ContextMenu<TValue = unknown> {
constructor: new (...args: ConstructorParameters<typeof ContextMenu<TValue>>) => ContextMenu<TValue>
}
/**
* ContextMenu from LiteGUI
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class ContextMenu {
options: IContextMenuOptions
parentMenu?: ContextMenu
root: ContextMenuDivElement
current_submenu?: ContextMenu
export class ContextMenu<TValue = unknown> {
options: IContextMenuOptions<TValue>
parentMenu?: ContextMenu<TValue>
root: ContextMenuDivElement<TValue>
current_submenu?: ContextMenu<TValue>
lock?: boolean
/**
@@ -35,7 +28,7 @@ export class ContextMenu {
* - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback
* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position
*/
constructor(values: (IContextMenuValue | string | null)[], options: IContextMenuOptions) {
constructor(values: (string | IContextMenuValue<TValue> | null)[], options: IContextMenuOptions<TValue>) {
options ||= {}
this.options = options
@@ -68,7 +61,7 @@ export class ContextMenu {
options.event = undefined
}
const root: ContextMenuDivElement = document.createElement("div")
const root: ContextMenuDivElement<TValue> = document.createElement("div")
let classes = "litegraph litecontextmenu litemenubar-panel"
if (options.className) classes += " " + options.className
root.className = classes
@@ -189,12 +182,12 @@ export class ContextMenu {
addItem(
name: string | null,
value: IContextMenuValue | string | null,
options: IContextMenuOptions,
value: string | IContextMenuValue<TValue> | null,
options: IContextMenuOptions<TValue>,
): HTMLElement {
options ||= {}
const element: ContextMenuDivElement = document.createElement("div")
const element: ContextMenuDivElement<TValue> = document.createElement("div")
element.className = "litemenu-entry submenu"
let disabled = false
@@ -246,7 +239,7 @@ export class ContextMenu {
element.setAttribute("aria-expanded", "true")
}
function inner_over(this: ContextMenuDivElement, e: MouseEvent) {
function inner_over(this: ContextMenuDivElement<TValue>, e: MouseEvent) {
const value = this.value
if (!value || !(value as IContextMenuValue).has_submenu) return
@@ -257,7 +250,7 @@ export class ContextMenu {
// menu option clicked
const that = this
function inner_onclick(this: ContextMenuDivElement, e: MouseEvent) {
function inner_onclick(this: ContextMenuDivElement<TValue>, e: MouseEvent) {
const value = this.value
let close_parent = true
@@ -358,7 +351,7 @@ export class ContextMenu {
}
// returns the top most menu
getTopMenu(): ContextMenu {
getTopMenu(): ContextMenu<TValue> {
return this.options.parentMenu
? this.options.parentMenu.getTopMenu()
: this

View File

@@ -4,6 +4,7 @@ import type {
CanvasColour,
ColorOption,
ConnectingLink,
ContextMenuDivElement,
Dictionary,
Direction,
IBoundaryNodes,
@@ -13,7 +14,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
INodeSlot,
IOptionalSlotData,
INodeSlotContextItem,
ISlotType,
LinkSegment,
NullableProperties,
@@ -437,8 +438,8 @@ export class LGraphCanvas implements ConnectionColorContext {
autoresize: boolean
static active_canvas: LGraphCanvas
static onMenuNodeOutputs?(
entries: (IOptionalSlotData<INodeOutputSlot> | null)[],
): (IOptionalSlotData<INodeOutputSlot> | null)[]
entries: (IContextMenuValue<INodeSlotContextItem> | null)[],
): (IContextMenuValue<INodeSlotContextItem> | null)[]
frame = 0
last_draw_time = 0
render_time = 0
@@ -746,7 +747,7 @@ export class LGraphCanvas implements ConnectionColorContext {
value: IContextMenuValue,
options: IContextMenuOptions,
event: MouseEvent,
prev_menu: ContextMenu,
prev_menu: ContextMenu<string>,
node: LGraphNode,
): void {
new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], {
@@ -769,7 +770,7 @@ export class LGraphCanvas implements ConnectionColorContext {
value: IContextMenuValue,
options: IContextMenuOptions,
event: MouseEvent,
prev_menu: ContextMenu,
prev_menu: ContextMenu<string>,
): void {
new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], {
event: event,
@@ -790,7 +791,7 @@ export class LGraphCanvas implements ConnectionColorContext {
value: IContextMenuValue,
options: IContextMenuOptions,
event: MouseEvent,
prev_menu: ContextMenu,
prev_menu: ContextMenu<string>,
): void {
new LiteGraph.ContextMenu(["Vertically", "Horizontally"], {
event,
@@ -809,8 +810,8 @@ export class LGraphCanvas implements ConnectionColorContext {
node: LGraphNode,
options: IContextMenuOptions,
e: MouseEvent,
prev_menu: ContextMenu,
callback?: (node: LGraphNode) => void,
prev_menu: ContextMenu<string>,
callback?: (node: LGraphNode | null) => void,
): boolean | undefined {
const canvas = LGraphCanvas.active_canvas
const ref_window = canvas.getCanvasWindow()
@@ -820,16 +821,16 @@ export class LGraphCanvas implements ConnectionColorContext {
inner_onMenuAdded("", prev_menu)
return false
type AddNodeMenu = Omit<IContextMenuValue, "callback"> & {
type AddNodeMenu = Omit<IContextMenuValue<string>, "callback"> & {
callback: (
value: { value: string },
event: Event,
mouseEvent: MouseEvent,
contextMenu: ContextMenu
contextMenu: ContextMenu<string>
) => void
}
function inner_onMenuAdded(base_category: string, prev_menu?: ContextMenu): void {
function inner_onMenuAdded(base_category: string, prev_menu?: ContextMenu<string>): void {
if (!graph) return
const categories = LiteGraph
@@ -913,7 +914,7 @@ export class LGraphCanvas implements ConnectionColorContext {
/** Unused - immediately overwritten */
_options: boolean,
e: MouseEvent,
prev_menu: ContextMenu,
prev_menu: ContextMenu<INodeSlotContextItem>,
node: LGraphNode,
): boolean | undefined {
if (!node) return
@@ -927,7 +928,7 @@ export class LGraphCanvas implements ConnectionColorContext {
? node.onGetInputs()
: undefined
let entries: (IOptionalSlotData<INodeInputSlot> | null)[] = []
let entries: (IContextMenuValue<INodeSlotContextItem> | null)[] = []
if (options) {
for (const entry of options) {
if (!entry) {
@@ -942,7 +943,7 @@ export class LGraphCanvas implements ConnectionColorContext {
}
entry[2].removable = true
const data: IOptionalSlotData<INodeInputSlot> = { content: label, value: entry }
const data: IContextMenuValue<INodeSlotContextItem> = { content: label, value: entry }
if (entry[1] == LiteGraph.ACTION) {
data.className = "event"
}
@@ -958,7 +959,7 @@ export class LGraphCanvas implements ConnectionColorContext {
return
}
new LiteGraph.ContextMenu(
new LiteGraph.ContextMenu<INodeSlotContextItem>(
entries,
{
event: e,
@@ -970,7 +971,7 @@ export class LGraphCanvas implements ConnectionColorContext {
ref_window,
)
function inner_clicked(v, e, prev) {
function inner_clicked(v: IContextMenuValue<INodeSlotContextItem>, e: MouseEvent, prev: ContextMenu<INodeSlotContextItem>) {
if (!node) return
v.callback?.call(that, node, v, e, prev)
@@ -1009,7 +1010,7 @@ export class LGraphCanvas implements ConnectionColorContext {
? node.onGetOutputs()
: undefined
let entries: (IOptionalSlotData<INodeOutputSlot> | null)[] = []
let entries: (IContextMenuValue<INodeSlotContextItem> | null)[] = []
if (options) {
for (const entry of options) {
if (!entry) {
@@ -1032,7 +1033,7 @@ export class LGraphCanvas implements ConnectionColorContext {
label = entry[2].label
}
entry[2].removable = true
const data: IOptionalSlotData<INodeOutputSlot> = { content: label, value: entry }
const data: IContextMenuValue<INodeSlotContextItem> = { content: label, value: entry }
if (entry[1] == LiteGraph.EVENT) {
data.className = "event"
}
@@ -1044,7 +1045,6 @@ export class LGraphCanvas implements ConnectionColorContext {
if (LiteGraph.do_add_triggers_slots) {
// canvas.allow_addOutSlot_onExecuted
if (node.findOutputSlot("onExecuted") == -1) {
// @ts-expect-error Events
entries.push({ content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, { nameLocked: true }], className: "event" })
}
}
@@ -1066,7 +1066,7 @@ export class LGraphCanvas implements ConnectionColorContext {
ref_window,
)
function inner_clicked(v, e, prev) {
function inner_clicked(v: IContextMenuValue<INodeSlotContextItem>, e: any, prev: any) {
if (!node) return
// TODO: This is a static method, so the below "that" appears broken.
@@ -1112,7 +1112,7 @@ export class LGraphCanvas implements ConnectionColorContext {
value: unknown,
options: unknown,
e: MouseEvent,
prev_menu: ContextMenu,
prev_menu: ContextMenu<string>,
node: LGraphNode,
): boolean | undefined {
if (!node || !node.properties) return
@@ -1120,7 +1120,7 @@ export class LGraphCanvas implements ConnectionColorContext {
const canvas = LGraphCanvas.active_canvas
const ref_window = canvas.getCanvasWindow()
const entries = []
const entries: IContextMenuValue<string>[] = []
for (const i in node.properties) {
value = node.properties[i] !== undefined ? node.properties[i] : " "
if (typeof value == "object")
@@ -1145,7 +1145,7 @@ export class LGraphCanvas implements ConnectionColorContext {
return
}
new LiteGraph.ContextMenu(
new LiteGraph.ContextMenu<string>(
entries,
{
event: e,
@@ -1158,7 +1158,7 @@ export class LGraphCanvas implements ConnectionColorContext {
ref_window,
)
function inner_clicked(v: { value: any }) {
function inner_clicked(this: ContextMenuDivElement, v: { value: any }) {
if (!node) return
const rect = this.getBoundingClientRect()
@@ -1203,10 +1203,10 @@ export class LGraphCanvas implements ConnectionColorContext {
// TODO refactor :: this is used fot title but not for properties!
static onShowPropertyEditor(
item: { property: string, type: string },
item: { property: keyof LGraphNode, type: string },
options: IContextMenuOptions,
e: MouseEvent,
menu: ContextMenu,
menu: ContextMenu<string>,
node: LGraphNode,
): void {
const property = item.property || "title"
@@ -1299,6 +1299,7 @@ export class LGraphCanvas implements ConnectionColorContext {
} else if (item.type == "Boolean") {
value = Boolean(value)
}
// @ts-expect-error Requires refactor.
node[property] = value
dialog.remove()
canvas.setDirty(true, true)
@@ -1418,15 +1419,15 @@ export class LGraphCanvas implements ConnectionColorContext {
/** @param value Parameter is never used */
static onMenuNodeColors(
value: IContextMenuValue,
value: IContextMenuValue<string>,
options: IContextMenuOptions,
e: MouseEvent,
menu: ContextMenu,
menu: ContextMenu<string>,
node: LGraphNode,
): boolean {
if (!node) throw "no node for color"
const values: IContextMenuValue[] = []
const values: IContextMenuValue<string, unknown, { value: string | null }>[] = []
values.push({
value: null,
content: "<span style='display: block; padding-left: 4px;'>No color</span>",
@@ -1446,14 +1447,14 @@ export class LGraphCanvas implements ConnectionColorContext {
}
values.push(value)
}
new LiteGraph.ContextMenu(values, {
new LiteGraph.ContextMenu<string>(values, {
event: e,
callback: inner_clicked,
parentMenu: menu,
node: node,
})
function inner_clicked(v: { value: string | null }) {
function inner_clicked(v: IContextMenuValue<string>) {
if (!node) return
const fApplyColor = function (item: IColorable) {
@@ -5650,7 +5651,7 @@ export class LGraphCanvas implements ConnectionColorContext {
const title = "data" in segment && segment.data != null
? segment.data.constructor.name
: null
const menu = new LiteGraph.ContextMenu(options, {
const menu = new LiteGraph.ContextMenu<string>(options, {
event: e,
title,
callback: inner_clicked.bind(this),
@@ -5905,7 +5906,7 @@ export class LGraphCanvas implements ConnectionColorContext {
}
// build menu
const menu = new LiteGraph.ContextMenu(options, {
const menu = new LiteGraph.ContextMenu<string>(options, {
event: opts.e,
title:
(slotX && slotX.name != ""
@@ -7222,7 +7223,7 @@ export class LGraphCanvas implements ConnectionColorContext {
// called by processContextMenu to extract the menu list
getNodeMenuOptions(node: LGraphNode): IContextMenuValue[] {
let options: IContextMenuValue<LGraphNode>[] = null
let options: IContextMenuValue<unknown, LGraphNode>[] = null
if (node.getMenuOptions) {
options = node.getMenuOptions(this)
@@ -7358,7 +7359,7 @@ export class LGraphCanvas implements ConnectionColorContext {
const ref_window = canvas.getCanvasWindow()
// TODO: Remove type kludge
let menu_info: (IContextMenuValue | string)[]
let menu_info: (IContextMenuValue | string | null)[]
const options: IContextMenuOptions = {
event: event,
callback: inner_option_clicked,

View File

@@ -300,7 +300,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
this.resizeTo([...this.children, ...this._nodes, ...nodes], padding)
}
getMenuOptions(): (IContextMenuValue | null)[] {
getMenuOptions(): (IContextMenuValue<string> | null)[] {
return [
{
content: this.pinned ? "Unpin" : "Pin",

View File

@@ -11,7 +11,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
INodeSlot,
IOptionalSlotData,
INodeSlotContextItem,
IPinnable,
ISlotType,
Point,
@@ -516,14 +516,14 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
onNodeInputAdd?(this: LGraphNode, value: unknown): void
onMenuNodeInputs?(
this: LGraphNode,
entries: IOptionalSlotData<INodeInputSlot>[],
): IOptionalSlotData<INodeInputSlot>[]
entries: (IContextMenuValue<INodeSlotContextItem> | null)[],
): (IContextMenuValue<INodeSlotContextItem> | null)[]
onMenuNodeOutputs?(
this: LGraphNode,
entries: IOptionalSlotData<INodeOutputSlot>[],
): IOptionalSlotData<INodeOutputSlot>[]
onGetInputs?(this: LGraphNode): INodeInputSlot[]
onGetOutputs?(this: LGraphNode): INodeOutputSlot[]
entries: (IContextMenuValue<INodeSlotContextItem> | null)[],
): (IContextMenuValue<INodeSlotContextItem> | null)[]
onGetInputs?(this: LGraphNode): INodeSlotContextItem[]
onGetOutputs?(this: LGraphNode): INodeSlotContextItem[]
onMouseUp?(this: LGraphNode, e: CanvasMouseEvent, pos: Point): void
onMouseEnter?(this: LGraphNode, e: CanvasMouseEvent): void
/** Blocks drag if return value is truthy. @param pos Offset from {@link LGraphNode.pos}. */

View File

@@ -289,22 +289,15 @@ export interface ConnectingLink extends IInputOrOutput {
afterRerouteId?: RerouteId
}
interface IContextMenuBase<TExtra = unknown> {
interface IContextMenuBase {
title?: string
className?: string
callback?(
value?: unknown,
options?: unknown,
event?: MouseEvent,
previous_menu?: ContextMenu,
extra?: TExtra,
): void | boolean
}
/** ContextMenu */
export interface IContextMenuOptions extends IContextMenuBase {
export interface IContextMenuOptions<TValue = unknown> extends IContextMenuBase {
ignore_item_callbacks?: boolean
parentMenu?: ContextMenu
parentMenu?: ContextMenu<TValue>
event?: MouseEvent
extra?: unknown
/** @deprecated Context menu scrolling is now controlled by the browser */
@@ -315,19 +308,42 @@ export interface IContextMenuOptions extends IContextMenuBase {
scale?: number
node?: LGraphNode
autoopen?: boolean
callback?(
value?: string | IContextMenuValue<TValue>,
options?: unknown,
event?: MouseEvent,
previous_menu?: ContextMenu<TValue>,
extra?: unknown,
): void | boolean
}
export interface IContextMenuValue<TExtra = unknown> extends IContextMenuBase<TExtra> {
value?: string
export interface IContextMenuValue<TValue = unknown, TExtra = unknown, TCallbackValue = unknown> extends IContextMenuBase {
value?: TValue
content: string | undefined
has_submenu?: boolean
disabled?: boolean
submenu?: IContextMenuSubmenu
submenu?: IContextMenuSubmenu<TValue>
property?: string
type?: string
slot?: IFoundSlot
callback?(
this: ContextMenuDivElement<TValue>,
value?: TCallbackValue,
options?: unknown,
event?: MouseEvent,
previous_menu?: ContextMenu<TValue>,
extra?: TExtra,
): void | boolean
}
export interface IContextMenuSubmenu extends IContextMenuOptions {
options: ConstructorParameters<typeof ContextMenu>[0]
export interface IContextMenuSubmenu<TValue = unknown> extends IContextMenuOptions<TValue> {
options: ConstructorParameters<typeof ContextMenu<TValue>>[0]
}
export interface ContextMenuDivElement<TValue = unknown> extends HTMLDivElement {
value?: string | IContextMenuValue<TValue>
onclick_callback?: never
closing_timer?: number
}
export type INodeSlotContextItem = [string, ISlotType, Partial<INodeInputSlot & INodeOutputSlot>]

View File

@@ -46,7 +46,7 @@ export type ContextMenuEventListener = (
value: IContextMenuItem,
options: IContextMenuOptions,
event: MouseEvent,
parentMenu: ContextMenu | undefined,
parentMenu: ContextMenu<unknown> | undefined,
node: LGraphNode,
) => boolean | void