Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import type {Bookmark} from "../bookmark.js";
|
||||
|
||||
import {app} from "scripts/app.js";
|
||||
import {NodeTypesString} from "../constants.js";
|
||||
import {reduceNodesDepthFirst} from "../utils.js";
|
||||
|
||||
const SHORTCUT_DEFAULTS = "1234567890abcdefghijklmnopqrstuvwxyz".split("");
|
||||
|
||||
class BookmarksService {
|
||||
/**
|
||||
* Gets a list of the current bookmarks within the current workflow.
|
||||
*/
|
||||
getCurrentBookmarks(): Bookmark[] {
|
||||
return reduceNodesDepthFirst<Bookmark[]>(app.graph.nodes, (n, acc) => {
|
||||
if (n.type === NodeTypesString.BOOKMARK) {
|
||||
acc.push(n as Bookmark);
|
||||
}
|
||||
}, []).sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
getExistingShortcuts() {
|
||||
const bookmarkNodes = this.getCurrentBookmarks();
|
||||
const usedShortcuts = new Set(bookmarkNodes.map((n) => n.shortcutKey));
|
||||
return usedShortcuts;
|
||||
}
|
||||
|
||||
getNextShortcut() {
|
||||
const existingShortcuts = this.getExistingShortcuts();
|
||||
return SHORTCUT_DEFAULTS.find((char) => !existingShortcuts.has(char)) ?? "1";
|
||||
}
|
||||
}
|
||||
|
||||
/** The BookmarksService singleton. */
|
||||
export const SERVICE = new BookmarksService();
|
||||
@@ -0,0 +1,40 @@
|
||||
// @ts-ignore
|
||||
import {rgthreeConfig} from "rgthree/config.js";
|
||||
import {getObjectValue, setObjectValue} from "rgthree/common/shared_utils.js";
|
||||
import {rgthreeApi} from "rgthree/common/rgthree_api.js";
|
||||
|
||||
/**
|
||||
* A singleton service exported as `SERVICE` to handle configuration routines.
|
||||
*/
|
||||
class ConfigService extends EventTarget {
|
||||
getConfigValue(key: string, def?: any) {
|
||||
return getObjectValue(rgthreeConfig, key, def);
|
||||
}
|
||||
|
||||
getFeatureValue(key: string, def?: any) {
|
||||
key = "features." + key.replace(/^features\./, "");
|
||||
return getObjectValue(rgthreeConfig, key, def);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an object of key:value changes it will send to the server and wait for a successful
|
||||
* response before setting the values on the local rgthreeConfig.
|
||||
*/
|
||||
async setConfigValues(changed: {[key: string]: any}) {
|
||||
const body = new FormData();
|
||||
body.append("json", JSON.stringify(changed));
|
||||
const response = await rgthreeApi.fetchJson("/config", {method: "POST", body});
|
||||
if (response.status === "ok") {
|
||||
for (const [key, value] of Object.entries(changed)) {
|
||||
setObjectValue(rgthreeConfig, key, value);
|
||||
this.dispatchEvent(new CustomEvent("config-change", {detail: {key, value}}));
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** The ConfigService singleton. */
|
||||
export const SERVICE = new ConfigService();
|
||||
@@ -0,0 +1,74 @@
|
||||
import type {DynamicContextNodeBase} from "../dynamic_context_base.js";
|
||||
|
||||
import {NodeTypesString} from "../constants.js";
|
||||
import {getConnectedOutputNodesAndFilterPassThroughs} from "../utils.js";
|
||||
import {INodeInputSlot, INodeOutputSlot, INodeSlot, LGraphNode} from "@comfyorg/frontend";
|
||||
|
||||
export let SERVICE: ContextService;
|
||||
|
||||
const OWNED_PREFIX = "+";
|
||||
const REGEX_PREFIX = /^[\+⚠️]\s*/;
|
||||
const REGEX_EMPTY_INPUT = /^\+\s*$/;
|
||||
|
||||
export function stripContextInputPrefixes(name: string) {
|
||||
return name.replace(REGEX_PREFIX, "");
|
||||
}
|
||||
|
||||
export function getContextOutputName(inputName: string) {
|
||||
if (inputName === "base_ctx") return "CONTEXT";
|
||||
return stripContextInputPrefixes(inputName).toUpperCase();
|
||||
}
|
||||
|
||||
export enum InputMutationOperation {
|
||||
"UNKNOWN",
|
||||
"ADDED",
|
||||
"REMOVED",
|
||||
"RENAMED",
|
||||
}
|
||||
|
||||
export type InputMutation = {
|
||||
operation: InputMutationOperation;
|
||||
node: DynamicContextNodeBase;
|
||||
slotIndex: number;
|
||||
slot: INodeSlot;
|
||||
};
|
||||
|
||||
export class ContextService {
|
||||
constructor() {
|
||||
if (SERVICE) {
|
||||
throw new Error("ContextService was already instantiated.");
|
||||
}
|
||||
}
|
||||
|
||||
onInputChanges(node: any, mutation: InputMutation) {
|
||||
const childCtxs = getConnectedOutputNodesAndFilterPassThroughs(
|
||||
node,
|
||||
node,
|
||||
0,
|
||||
) as DynamicContextNodeBase[];
|
||||
for (const childCtx of childCtxs) {
|
||||
childCtx.handleUpstreamMutation(mutation);
|
||||
}
|
||||
}
|
||||
|
||||
getDynamicContextInputsData(node: DynamicContextNodeBase) {
|
||||
return node
|
||||
.getContextInputsList()
|
||||
.map((input: INodeInputSlot, index: number) => ({
|
||||
name: stripContextInputPrefixes(input.name),
|
||||
type: String(input.type),
|
||||
index,
|
||||
}))
|
||||
.filter((i) => i.type !== "*");
|
||||
}
|
||||
|
||||
getDynamicContextOutputsData(node: LGraphNode) {
|
||||
return node.outputs.map((output: INodeOutputSlot, index: number) => ({
|
||||
name: stripContextInputPrefixes(output.name),
|
||||
type: String(output.type),
|
||||
index,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
SERVICE = new ContextService();
|
||||
@@ -0,0 +1,231 @@
|
||||
import type {LGraphGroup as TLGraphGroup} from "@comfyorg/frontend";
|
||||
import type {BaseFastGroupsModeChanger} from "../fast_groups_muter.js";
|
||||
|
||||
import {app} from "scripts/app.js";
|
||||
import {getGraphDependantNodeKey, getGroupNodes, reduceNodesDepthFirst} from "../utils.js";
|
||||
|
||||
type Vector4 = [number, number, number, number];
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A service that keeps global state that can be shared by multiple FastGroupsMuter or
|
||||
* FastGroupsBypasser nodes rather than calculate it on it's own.
|
||||
*/
|
||||
class FastGroupsService {
|
||||
private msThreshold = 400;
|
||||
private msLastUnsorted = 0;
|
||||
private msLastAlpha = 0;
|
||||
private msLastPosition = 0;
|
||||
|
||||
private groupsUnsorted: TLGraphGroup[] = [];
|
||||
private groupsSortedAlpha: TLGraphGroup[] = [];
|
||||
private groupsSortedPosition: TLGraphGroup[] = [];
|
||||
|
||||
private readonly fastGroupNodes: BaseFastGroupsModeChanger[] = [];
|
||||
|
||||
private runScheduledForMs: number | null = null;
|
||||
private runScheduleTimeout: number | null = null;
|
||||
private runScheduleAnimation: number | null = null;
|
||||
|
||||
private cachedNodeBoundings: {[key: string]: Vector4} | null = null;
|
||||
|
||||
constructor() {
|
||||
// Don't need to do anything, wait until a signal.
|
||||
}
|
||||
|
||||
addFastGroupNode(node: BaseFastGroupsModeChanger) {
|
||||
this.fastGroupNodes.push(node);
|
||||
// Schedule it because the node may not be ready to refreshWidgets (like, when added it may
|
||||
// not have cloned properties to filter against, etc.).
|
||||
this.scheduleRun(8);
|
||||
}
|
||||
|
||||
removeFastGroupNode(node: BaseFastGroupsModeChanger) {
|
||||
const index = this.fastGroupNodes.indexOf(node);
|
||||
if (index > -1) {
|
||||
this.fastGroupNodes.splice(index, 1);
|
||||
}
|
||||
// If we have no more group nodes, then clear out data; it could be because of a canvas clear.
|
||||
if (!this.fastGroupNodes?.length) {
|
||||
this.clearScheduledRun();
|
||||
this.groupsUnsorted = [];
|
||||
this.groupsSortedAlpha = [];
|
||||
this.groupsSortedPosition = [];
|
||||
}
|
||||
}
|
||||
|
||||
private run() {
|
||||
// We only run if we're scheduled, so if we're not, then bail.
|
||||
if (!this.runScheduledForMs) {
|
||||
return;
|
||||
}
|
||||
for (const node of this.fastGroupNodes) {
|
||||
node.refreshWidgets();
|
||||
}
|
||||
this.clearScheduledRun();
|
||||
this.scheduleRun();
|
||||
}
|
||||
|
||||
private scheduleRun(ms = 500) {
|
||||
// If we got a request for an immediate schedule and already have on scheduled for longer, then
|
||||
// cancel the long one to expediate a fast one.
|
||||
if (this.runScheduledForMs && ms < this.runScheduledForMs) {
|
||||
this.clearScheduledRun();
|
||||
}
|
||||
if (!this.runScheduledForMs && this.fastGroupNodes.length) {
|
||||
this.runScheduledForMs = ms;
|
||||
this.runScheduleTimeout = setTimeout(() => {
|
||||
this.runScheduleAnimation = requestAnimationFrame(() => this.run());
|
||||
}, ms);
|
||||
}
|
||||
}
|
||||
|
||||
private clearScheduledRun() {
|
||||
this.runScheduleTimeout && clearTimeout(this.runScheduleTimeout);
|
||||
this.runScheduleAnimation && cancelAnimationFrame(this.runScheduleAnimation);
|
||||
this.runScheduleTimeout = null;
|
||||
this.runScheduleAnimation = null;
|
||||
this.runScheduledForMs = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boundings for all nodes on the graph, then clears it after a short delay. This is
|
||||
* to increase efficiency by caching the nodes' boundings when multiple groups are on the page.
|
||||
*/
|
||||
getBoundingsForAllNodes() {
|
||||
if (!this.cachedNodeBoundings) {
|
||||
this.cachedNodeBoundings = reduceNodesDepthFirst(
|
||||
app.graph._nodes,
|
||||
(node, acc) => {
|
||||
let bounds = node.getBounding();
|
||||
// If the bounds are zero'ed out, then we could be a subgraph that hasn't rendered yet and
|
||||
// need to update them.
|
||||
if (bounds[0] === 0 && bounds[1] === 0 && bounds[2] === 0 && bounds[3] === 0) {
|
||||
const ctx = node.graph?.primaryCanvas?.canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
node.updateArea(ctx);
|
||||
bounds = node.getBounding();
|
||||
}
|
||||
}
|
||||
acc[getGraphDependantNodeKey(node)] = bounds as Vector4;
|
||||
},
|
||||
{} as {[key: string]: Vector4},
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.cachedNodeBoundings = null;
|
||||
}, 50);
|
||||
}
|
||||
return this.cachedNodeBoundings;
|
||||
}
|
||||
|
||||
/**
|
||||
* This overrides `TLGraphGroup.prototype.recomputeInsideNodes` to be much more efficient when
|
||||
* calculating for many groups at once (only compute all nodes once in `getBoundingsForAllNodes`).
|
||||
*/
|
||||
recomputeInsideNodesForGroup(group: TLGraphGroup) {
|
||||
// If the canvas is currently being dragged (includes if a group is being dragged around) then
|
||||
// don't recompute anything.
|
||||
if (app.canvas.isDragging) return;
|
||||
const cachedBoundings = this.getBoundingsForAllNodes();
|
||||
const nodes = group.graph!.nodes;
|
||||
group._children.clear();
|
||||
group.nodes.length = 0;
|
||||
|
||||
for (const node of nodes) {
|
||||
const nodeBounding = cachedBoundings[getGraphDependantNodeKey(node)];
|
||||
const nodeCenter =
|
||||
nodeBounding &&
|
||||
([nodeBounding[0] + nodeBounding[2] * 0.5, nodeBounding[1] + nodeBounding[3] * 0.5] as [
|
||||
number,
|
||||
number,
|
||||
]);
|
||||
if (nodeCenter) {
|
||||
const grouBounds = group._bounding as unknown as [number, number, number, number];
|
||||
if (
|
||||
nodeCenter[0] >= grouBounds[0] &&
|
||||
nodeCenter[0] < grouBounds[0] + grouBounds[2] &&
|
||||
nodeCenter[1] >= grouBounds[1] &&
|
||||
nodeCenter[1] < grouBounds[1] + grouBounds[3]
|
||||
) {
|
||||
group._children.add(node);
|
||||
group.nodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything goes through getGroupsUnsorted, so we only get groups once. However, LiteGraph's
|
||||
* `recomputeInsideNodes` is inefficient when calling multiple groups (it iterates over all nodes
|
||||
* each time). So, we'll do our own dang thing, once.
|
||||
*/
|
||||
private getGroupsUnsorted(now: number) {
|
||||
const canvas = app.canvas;
|
||||
const graph = canvas.getCurrentGraph() ?? app.graph;
|
||||
|
||||
if (
|
||||
// Don't recalculate nodes if we're moving a group (added by ComfyUI in app.js)
|
||||
// TODO: This doesn't look available anymore... ?
|
||||
!canvas.selected_group_moving &&
|
||||
(!this.groupsUnsorted.length || now - this.msLastUnsorted > this.msThreshold)
|
||||
) {
|
||||
this.groupsUnsorted = [...graph._groups];
|
||||
const subgraphs = graph.subgraphs?.values();
|
||||
if (subgraphs) {
|
||||
let s;
|
||||
while ((s = subgraphs.next().value)) this.groupsUnsorted.push(...(s.groups ?? []));
|
||||
}
|
||||
for (const group of this.groupsUnsorted) {
|
||||
this.recomputeInsideNodesForGroup(group);
|
||||
group.rgthree_hasAnyActiveNode = getGroupNodes(group).some(
|
||||
(n) => n.mode === LiteGraph.ALWAYS,
|
||||
);
|
||||
}
|
||||
this.msLastUnsorted = now;
|
||||
}
|
||||
return this.groupsUnsorted;
|
||||
}
|
||||
|
||||
private getGroupsAlpha(now: number) {
|
||||
if (!this.groupsSortedAlpha.length || now - this.msLastAlpha > this.msThreshold) {
|
||||
this.groupsSortedAlpha = [...this.getGroupsUnsorted(now)].sort((a, b) => {
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
this.msLastAlpha = now;
|
||||
}
|
||||
return this.groupsSortedAlpha;
|
||||
}
|
||||
|
||||
private getGroupsPosition(now: number) {
|
||||
if (!this.groupsSortedPosition.length || now - this.msLastPosition > this.msThreshold) {
|
||||
this.groupsSortedPosition = [...this.getGroupsUnsorted(now)].sort((a, b) => {
|
||||
// Sort by y, then x, clamped to 30.
|
||||
const aY = Math.floor(a._pos[1] / 30);
|
||||
const bY = Math.floor(b._pos[1] / 30);
|
||||
if (aY == bY) {
|
||||
const aX = Math.floor(a._pos[0] / 30);
|
||||
const bX = Math.floor(b._pos[0] / 30);
|
||||
return aX - bX;
|
||||
}
|
||||
return aY - bY;
|
||||
});
|
||||
this.msLastPosition = now;
|
||||
}
|
||||
return this.groupsSortedPosition;
|
||||
}
|
||||
|
||||
getGroups(sort?: string) {
|
||||
const now = +new Date();
|
||||
if (sort === "alphanumeric") {
|
||||
return this.getGroupsAlpha(now);
|
||||
}
|
||||
if (sort === "position") {
|
||||
return this.getGroupsPosition(now);
|
||||
}
|
||||
return this.getGroupsUnsorted(now);
|
||||
}
|
||||
}
|
||||
|
||||
/** The FastGroupsService singleton. */
|
||||
export const SERVICE = new FastGroupsService();
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* A service responsible for capturing keys within LiteGraph's canvas, and outside of it, allowing
|
||||
* nodes and other services to confidently determine what's going on.
|
||||
*/
|
||||
class KeyEventService extends EventTarget {
|
||||
readonly downKeys: { [key: string]: boolean } = {};
|
||||
readonly shiftDownKeys: { [key: string]: boolean } = {};
|
||||
|
||||
ctrlKey = false;
|
||||
altKey = false;
|
||||
metaKey = false;
|
||||
shiftKey = false;
|
||||
|
||||
private readonly isMac: boolean = !!(
|
||||
navigator.platform?.toLocaleUpperCase().startsWith("MAC") ||
|
||||
(navigator as any).userAgentData?.platform?.toLocaleUpperCase().startsWith("MAC")
|
||||
);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
const that = this;
|
||||
// [🤮] Sometimes ComfyUI and/or LiteGraph stop propagation of key events which makes it hard
|
||||
// to determine if keys are currently pressed. To attempt to get around this, we'll hijack
|
||||
// LiteGraph's processKey to try to get better consistency.
|
||||
const processKey = LGraphCanvas.prototype.processKey;
|
||||
LGraphCanvas.prototype.processKey = function (e: KeyboardEvent) {
|
||||
if (e.type === "keydown" || e.type === "keyup") {
|
||||
that.handleKeyDownOrUp(e);
|
||||
}
|
||||
return processKey.apply(this, [...arguments] as any) as any;
|
||||
};
|
||||
|
||||
// Now that ComfyUI has more non-canvas UI (like the top bar), we listen on window as well, and
|
||||
// de-dupe when we get multiple events from both window and/or LiteGraph.
|
||||
window.addEventListener("keydown", (e) => {
|
||||
that.handleKeyDownOrUp(e);
|
||||
});
|
||||
window.addEventListener("keyup", (e) => {
|
||||
that.handleKeyDownOrUp(e);
|
||||
});
|
||||
|
||||
// If we get a visibilitychange, then clear the keys since we can't listen for keys up/down when
|
||||
// not visible.
|
||||
document.addEventListener("visibilitychange", (e) => {
|
||||
this.clearKeydowns();
|
||||
});
|
||||
|
||||
// If we get a blur, then also clear the keys since we can't listen for keys up/down when
|
||||
// blurred. This can happen w/o a visibilitychange, like a browser alert.
|
||||
window.addEventListener("blur", (e) => {
|
||||
this.clearKeydowns();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new queue item, unless the last is the same.
|
||||
*/
|
||||
handleKeyDownOrUp(e: KeyboardEvent) {
|
||||
const key = e.key.toLocaleUpperCase();
|
||||
// If we're already down, or already up, then ignore and don't fire.
|
||||
if ((e.type === 'keydown' && this.downKeys[key] === true)
|
||||
|| (e.type === 'keyup' && this.downKeys[key] === undefined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctrlKey = !!e.ctrlKey;
|
||||
this.altKey = !!e.altKey;
|
||||
this.metaKey = !!e.metaKey;
|
||||
this.shiftKey = !!e.shiftKey;
|
||||
if (e.type === "keydown") {
|
||||
this.downKeys[key] = true;
|
||||
this.dispatchCustomEvent("keydown", { originalEvent: e });
|
||||
|
||||
// If SHIFT is pressed down as well, then we need to keep track of this separetly to "release"
|
||||
// it once SHIFT is also released.
|
||||
if (this.shiftKey && key !== 'SHIFT') {
|
||||
this.shiftDownKeys[key] = true;
|
||||
}
|
||||
} else if (e.type === "keyup") {
|
||||
// See https://github.com/rgthree/rgthree-comfy/issues/238
|
||||
// A little bit of a hack, but Mac reportedly does something odd with copy/paste. ComfyUI
|
||||
// gobbles the copy event propagation, but it happens for paste too and reportedly 'Enter' which
|
||||
// I can't find a reason for in LiteGraph/comfy. So, for Mac only, whenever we lift a Command
|
||||
// (META) key, we'll also clear any other keys.
|
||||
if (key === "META" && this.isMac) {
|
||||
this.clearKeydowns();
|
||||
} else {
|
||||
delete this.downKeys[key];
|
||||
}
|
||||
|
||||
// If we're releasing the SHIFT key, then we may also be releasing all other keys we pressed
|
||||
// during the SHIFT key as well. We should get an additional keydown for them after.
|
||||
if (key === 'SHIFT') {
|
||||
for (const key in this.shiftDownKeys) {
|
||||
delete this.downKeys[key];
|
||||
delete this.shiftDownKeys[key];
|
||||
}
|
||||
}
|
||||
this.dispatchCustomEvent("keyup", { originalEvent: e });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private clearKeydowns() {
|
||||
this.ctrlKey = false;
|
||||
this.altKey = false;
|
||||
this.metaKey = false;
|
||||
this.shiftKey = false;
|
||||
for (const key in this.downKeys) delete this.downKeys[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `dispatchEvent` for easier CustomEvent dispatching.
|
||||
*/
|
||||
private dispatchCustomEvent(event: string, detail?: any) {
|
||||
if (detail != null) {
|
||||
return this.dispatchEvent(new CustomEvent(event, { detail }));
|
||||
}
|
||||
return this.dispatchEvent(new CustomEvent(event));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a shortcut string.
|
||||
*
|
||||
* - 's' => ['S']
|
||||
* - 'shift + c' => ['SHIFT', 'C']
|
||||
* - 'shift + meta + @' => ['SHIFT', 'META', '@']
|
||||
* - 'shift + + + @' => ['SHIFT', '__PLUS__', '=']
|
||||
* - '+ + p' => ['__PLUS__', 'P']
|
||||
*/
|
||||
private getKeysFromShortcut(shortcut: string | string[]) {
|
||||
let keys;
|
||||
if (typeof shortcut === "string") {
|
||||
// Rip all spaces out. Note, Comfy swallows space, so we don't have to handle it. Otherwise,
|
||||
// we would require space to be fed as "Space" or "Spacebar" instead of " ".
|
||||
shortcut = shortcut.replace(/\s/g, "");
|
||||
// Change a real "+" to something we can encode.
|
||||
shortcut = shortcut.replace(/^\+/, "__PLUS__").replace(/\+\+/, "+__PLUS__");
|
||||
keys = shortcut.split("+").map((i) => i.replace("__PLUS__", "+"));
|
||||
} else {
|
||||
keys = [...shortcut];
|
||||
}
|
||||
return keys.map((k) => k.toLocaleUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all keys passed in are down.
|
||||
*/
|
||||
areAllKeysDown(keys: string | string[]) {
|
||||
keys = this.getKeysFromShortcut(keys);
|
||||
return keys.every((k) => {
|
||||
return this.downKeys[k];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if only the keys passed in are down; optionally and additionally allowing "shift" key.
|
||||
*/
|
||||
areOnlyKeysDown(keys: string | string[], alsoAllowShift = false) {
|
||||
keys = this.getKeysFromShortcut(keys);
|
||||
const allKeysDown = this.areAllKeysDown(keys);
|
||||
const downKeysLength = Object.values(this.downKeys).length;
|
||||
// All keys are down and they're the only ones.
|
||||
if (allKeysDown && keys.length === downKeysLength) {
|
||||
return true;
|
||||
}
|
||||
// Special case allowing the shift key in addition to the shortcut keys. This helps when a user
|
||||
// may had originally defined "$" as a shortcut, but needs to press "shift + $" since it's an
|
||||
// upper key character, etc.
|
||||
if (alsoAllowShift && !keys.includes("SHIFT") && keys.length === downKeysLength - 1) {
|
||||
// If we're holding down shift, have one extra key held down, and the original keys don't
|
||||
// include shift, then we're good to go.
|
||||
return allKeysDown && this.areAllKeysDown(["SHIFT"]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The KeyEventService singleton. */
|
||||
export const SERVICE = new KeyEventService();
|
||||
Reference in New Issue
Block a user