Compare commits
76 Commits
master
...
danieljgei
Author | SHA1 | Date | |
---|---|---|---|
![]() |
baf3ab7d81 | ||
![]() |
ef0fcc1537 | ||
![]() |
ec26aeead2 | ||
![]() |
62f5475c4a | ||
![]() |
7225915b82 | ||
![]() |
8eb3191b3f | ||
![]() |
4d6d6cf129 | ||
![]() |
208285b7ba | ||
![]() |
372a4868da | ||
![]() |
05800d8599 | ||
![]() |
1f496d9f64 | ||
![]() |
e0221ddf20 | ||
![]() |
1bd86942f3 | ||
![]() |
fd9a172da9 | ||
![]() |
1f9847ed98 | ||
![]() |
4e4802b19e | ||
![]() |
23eb08088e | ||
![]() |
e8a6053251 | ||
![]() |
456433e8f0 | ||
![]() |
38e3a4e8e1 | ||
![]() |
27a8cda8fd | ||
![]() |
dd5053149a | ||
![]() |
40ec02b280 | ||
![]() |
b81aa19ff9 | ||
![]() |
e4ddd08bb1 | ||
![]() |
795176b256 | ||
![]() |
be057bde39 | ||
![]() |
94f4b727bb | ||
![]() |
63698572db | ||
![]() |
ab3467973f | ||
![]() |
91fe07d9c5 | ||
![]() |
28cc821047 | ||
![]() |
7dc728a459 | ||
![]() |
12c651af6d | ||
![]() |
9d0cafe10b | ||
![]() |
fb24221587 | ||
![]() |
ef347cc685 | ||
![]() |
2d3b9e0c66 | ||
![]() |
bdb0dd064b | ||
![]() |
b17ed4dc29 | ||
![]() |
b988f67759 | ||
![]() |
089aaa8792 | ||
![]() |
28261c4b29 | ||
![]() |
3fbed86d3e | ||
![]() |
38b3d90fa6 | ||
![]() |
82b597ab8b | ||
![]() |
4c939cefad | ||
![]() |
8f0d9f5230 | ||
![]() |
fcde0ac3de | ||
![]() |
b07dfba4b8 | ||
![]() |
1089cdb278 | ||
![]() |
7246a6b17a | ||
![]() |
04a96caf78 | ||
![]() |
14c6ea938a | ||
![]() |
87aba3f619 | ||
![]() |
c8d4e8c421 | ||
![]() |
512e506798 | ||
![]() |
b4e742bda0 | ||
![]() |
5a3f4fd08f | ||
![]() |
34515f2952 | ||
![]() |
08f430b3ac | ||
![]() |
59e74f94e6 | ||
![]() |
ddc393bd9d | ||
![]() |
9e5948ac28 | ||
![]() |
f86d0f9102 | ||
![]() |
ace031e992 | ||
![]() |
45faf7d58f | ||
![]() |
8c558a0f33 | ||
![]() |
65059cb166 | ||
![]() |
9158e2d989 | ||
![]() |
12da1862a0 | ||
![]() |
67fb3210ab | ||
![]() |
13d69d8cef | ||
![]() |
0f6ad916c0 | ||
![]() |
9ee2bf36cf | ||
![]() |
86f5c2ebcf |
@ -5,6 +5,7 @@ import { trackEvent } from "../src/analytics";
|
||||
import { getDefaultAppState } from "../src/appState";
|
||||
import { ErrorDialog } from "../src/components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
|
||||
import { useMathSubtype } from "../src/element/subtypes/mathjax";
|
||||
import {
|
||||
APP_NAME,
|
||||
EVENT,
|
||||
@ -303,6 +304,8 @@ const ExcalidrawWrapper = () => {
|
||||
const [excalidrawAPI, excalidrawRefCallback] =
|
||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||
|
||||
useMathSubtype(excalidrawAPI);
|
||||
|
||||
const [collabAPI] = useAtom(collabAPIAtom);
|
||||
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||
|
@ -31,6 +31,7 @@
|
||||
"browser-fs-access": "0.29.1",
|
||||
"canvas-roundrect-polyfill": "0.0.1",
|
||||
"clsx": "1.1.1",
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"fake-indexeddb": "3.1.7",
|
||||
@ -40,18 +41,22 @@
|
||||
"image-blob-reduce": "3.0.1",
|
||||
"jotai": "1.13.1",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"mathjax-full": "https://github.com/MathJax/MathJax-src#develop",
|
||||
"nanoid": "3.3.3",
|
||||
"open-color": "1.9.1",
|
||||
"pako": "1.0.11",
|
||||
"patch-package": "8.0.0",
|
||||
"perfect-freehand": "1.2.0",
|
||||
"pica": "7.1.1",
|
||||
"png-chunk-text": "1.0.0",
|
||||
"png-chunks-encode": "1.0.0",
|
||||
"png-chunks-extract": "1.0.0",
|
||||
"points-on-curve": "0.2.0",
|
||||
"postinstall-postinstall": "2.1.0",
|
||||
"pwacompat": "2.0.17",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"replace-in-file": "7.0.1",
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.51.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
@ -111,6 +116,7 @@
|
||||
"fix": "yarn fix:other && yarn fix:code",
|
||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||
"postinstall": "patch-package && yarn --cwd node_modules/mathjax-full compile-mjs && node scripts/beta-mathjax-import-paths.js",
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"start": "vite",
|
||||
|
126
patches/mathjax-full+4.0.0-beta.3.patch
Normal file
126
patches/mathjax-full+4.0.0-beta.3.patch
Normal file
@ -0,0 +1,126 @@
|
||||
diff --git a/node_modules/mathjax-full/ts/input/asciimath/legacy/shim.js b/node_modules/mathjax-full/ts/input/asciimath/legacy/shim.js
|
||||
index 3b228bb9..c8bcdea5 100644
|
||||
--- a/node_modules/mathjax-full/ts/input/asciimath/legacy/shim.js
|
||||
+++ b/node_modules/mathjax-full/ts/input/asciimath/legacy/shim.js
|
||||
@@ -1,4 +1,4 @@
|
||||
-MathJax = Object.assign(global.MathJax || {}, require("./MathJax.js").MathJax);
|
||||
+window.MathJax = Object.assign(window.MathJax || {}, require("./MathJax.js").MathJax);
|
||||
|
||||
//
|
||||
// Load component-based configuration, if any
|
||||
@@ -13,10 +13,13 @@ MathJax.Ajax.Preloading(
|
||||
"[MathJax]/jax/element/mml/jax.js"
|
||||
);
|
||||
|
||||
-require("./jax/element/mml/jax.js");
|
||||
-require("./jax/input/AsciiMath/config.js");
|
||||
-require("./jax/input/AsciiMath/jax.js");
|
||||
+module.exports.AsciiMath = void 0;
|
||||
+(async () => {
|
||||
+ await import("./jax/element/mml/jax.js");
|
||||
+ await import("./jax/input/AsciiMath/config.js");
|
||||
+ await import("./jax/input/AsciiMath/jax.js");
|
||||
|
||||
-require("./jax/element/MmlNode.js");
|
||||
+ await import("./jax/element/MmlNode.js");
|
||||
|
||||
-module.exports.AsciiMath = MathJax.InputJax.AsciiMath;
|
||||
+ module.exports.AsciiMath = MathJax.InputJax.AsciiMath;
|
||||
+})();
|
||||
diff --git a/node_modules/mathjax-full/ts/input/asciimath/legacy/MathJax.js b/node_modules/mathjax-full/ts/input/asciimath/legacy/MathJax.js
|
||||
index 853b0a0e..1e009028 100644
|
||||
--- a/node_modules/mathjax-full/ts/input/asciimath/legacy/MathJax.js
|
||||
+++ b/node_modules/mathjax-full/ts/input/asciimath/legacy/MathJax.js
|
||||
@@ -19,7 +19,7 @@ exports.MathJax = MathJax;
|
||||
return obj;
|
||||
};
|
||||
var CONSTRUCTOR = function () {
|
||||
- return function () {return arguments.callee.Init.call(this,arguments)};
|
||||
+ return function fn() {return fn.Init.call(this,Object.assign(arguments,{call:fn}))};
|
||||
};
|
||||
|
||||
BASE.Object = OBJECT({
|
||||
@@ -40,7 +40,7 @@ exports.MathJax = MathJax;
|
||||
Init: function (args) {
|
||||
var obj = this;
|
||||
if (args.length === 1 && args[0] === PROTO) {return obj}
|
||||
- if (!(obj instanceof args.callee)) {obj = new args.callee(PROTO)}
|
||||
+ if (!(obj instanceof args.call)) {obj = new args.call(PROTO)}
|
||||
return obj.Init.apply(obj,args) || obj;
|
||||
},
|
||||
|
||||
@@ -65,7 +65,7 @@ exports.MathJax = MathJax;
|
||||
|
||||
prototype: {
|
||||
Init: function () {},
|
||||
- SUPER: function (fn) {return fn.callee.SUPER},
|
||||
+ SUPER: function (fn) {return fn.SUPER},
|
||||
can: function (method) {return typeof(this[method]) === "function"},
|
||||
has: function (property) {return typeof(this[property]) !== "undefined"},
|
||||
isa: function (obj) {return (obj instanceof Object) && (this instanceof obj)}
|
||||
@@ -177,7 +177,7 @@ exports.MathJax = MathJax;
|
||||
// Create a callback from an associative array
|
||||
//
|
||||
var CALLBACK = function (data) {
|
||||
- var cb = function () {return arguments.callee.execute.apply(arguments.callee,arguments)};
|
||||
+ var cb = function fn() {return fn.execute.apply(fn,arguments)};
|
||||
for (var id in CALLBACK.prototype) {
|
||||
if (CALLBACK.prototype.hasOwnProperty(id)) {
|
||||
if (typeof(data[id]) !== 'undefined') {cb[id] = data[id]}
|
||||
diff --git a/node_modules/mathjax-full/ts/input/asciimath/legacy/jax/element/mml/jax.js b/node_modules/mathjax-full/ts/input/asciimath/legacy/jax/element/mml/jax.js
|
||||
index 96fb9186..473aca11 100644
|
||||
--- a/node_modules/mathjax-full/ts/input/asciimath/legacy/jax/element/mml/jax.js
|
||||
+++ b/node_modules/mathjax-full/ts/input/asciimath/legacy/jax/element/mml/jax.js
|
||||
@@ -813,9 +813,9 @@ MathJax.ElementJax.mml.Augment({
|
||||
if (!(this.isEmbellished()) || typeof(this.core) === "undefined") {return this}
|
||||
return this.data[this.core].CoreMO();
|
||||
},
|
||||
- toString: function () {
|
||||
+ toString: function fn() {
|
||||
if (this.inferred) {return '[' + this.data.join(',') + ']'}
|
||||
- return this.SUPER(arguments).toString.call(this);
|
||||
+ return this.SUPER(fn).toString.call(this);
|
||||
},
|
||||
setTeXclass: function (prev) {
|
||||
var i, m = this.data.length;
|
||||
@@ -1196,12 +1196,12 @@ MathJax.ElementJax.mml.Augment({
|
||||
}
|
||||
},
|
||||
linebreakContainer: true,
|
||||
- Append: function () {
|
||||
+ Append: function fn() {
|
||||
for (var i = 0, m = arguments.length; i < m; i++) {
|
||||
if (!((arguments[i] instanceof MML.mtr) ||
|
||||
(arguments[i] instanceof MML.mlabeledtr))) {arguments[i] = MML.mtr(arguments[i])}
|
||||
}
|
||||
- this.SUPER(arguments).Append.apply(this,arguments);
|
||||
+ this.SUPER(fn).Append.apply(this,arguments);
|
||||
},
|
||||
setTeXclass: MML.mbase.setSeparateTeXclasses
|
||||
});
|
||||
@@ -1221,11 +1221,11 @@ MathJax.ElementJax.mml.Augment({
|
||||
mtable: {rowalign: true, columnalign: true, groupalign: true}
|
||||
},
|
||||
linebreakContainer: true,
|
||||
- Append: function () {
|
||||
+ Append: function fn() {
|
||||
for (var i = 0, m = arguments.length; i < m; i++) {
|
||||
if (!(arguments[i] instanceof MML.mtd)) {arguments[i] = MML.mtd(arguments[i])}
|
||||
}
|
||||
- this.SUPER(arguments).Append.apply(this,arguments);
|
||||
+ this.SUPER(fn).Append.apply(this,arguments);
|
||||
},
|
||||
setTeXclass: MML.mbase.setSeparateTeXclasses
|
||||
});
|
||||
@@ -1420,9 +1420,9 @@ MathJax.ElementJax.mml.Augment({
|
||||
|
||||
MML.xml = MML.mbase.Subclass({
|
||||
type: "xml",
|
||||
- Init: function () {
|
||||
+ Init: function fn() {
|
||||
this.div = document.createElement("div");
|
||||
- return this.SUPER(arguments).Init.apply(this,arguments);
|
||||
+ return this.SUPER(fn).Init.apply(this,arguments);
|
||||
},
|
||||
Append: function () {
|
||||
for (var i = 0, m = arguments.length; i < m; i++) {
|
10
scripts/beta-mathjax-import-paths.js
Normal file
10
scripts/beta-mathjax-import-paths.js
Normal file
@ -0,0 +1,10 @@
|
||||
// When building MathJax 4.0-beta from source within the Excalidraw tree, some
|
||||
// import paths don't properly translate from `ts/` to `mjs/`. This makes the
|
||||
// Excalidraw build process parse MathJax TypeScript files. The resulting error
|
||||
// messages do not occur if MathJax was built from source outside the
|
||||
// Excalidraw tree. The following regexp eliminates those error messages.
|
||||
require("replace-in-file").sync({
|
||||
files: "node_modules/mathjax-full/mjs/**/*",
|
||||
from: /mathjax-full\/ts/g,
|
||||
to: "mathjax-full/mjs",
|
||||
});
|
@ -10,7 +10,7 @@ import {
|
||||
computeBoundTextPosition,
|
||||
computeContainerDimensionForBoundText,
|
||||
getBoundTextElement,
|
||||
measureText,
|
||||
measureTextElement,
|
||||
redrawTextBoundingBox,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
@ -31,7 +31,6 @@ import {
|
||||
} from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { getFontString } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionUnbindText = register({
|
||||
@ -48,10 +47,11 @@ export const actionUnbindText = register({
|
||||
selectedElements.forEach((element) => {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const { width, height, baseline } = measureText(
|
||||
boundTextElement.originalText,
|
||||
getFontString(boundTextElement),
|
||||
boundTextElement.lineHeight,
|
||||
const { width, height, baseline } = measureTextElement(
|
||||
boundTextElement,
|
||||
{
|
||||
text: boundTextElement.originalText,
|
||||
},
|
||||
);
|
||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||
element.id,
|
||||
|
@ -2,10 +2,10 @@ import React from "react";
|
||||
import {
|
||||
Action,
|
||||
UpdaterFn,
|
||||
ActionName,
|
||||
ActionResult,
|
||||
PanelComponentProps,
|
||||
ActionSource,
|
||||
ActionPredicateFn,
|
||||
} from "./types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
@ -40,7 +40,8 @@ const trackAction = (
|
||||
};
|
||||
|
||||
export class ActionManager {
|
||||
actions = {} as Record<ActionName, Action>;
|
||||
actions = {} as Record<Action["name"], Action>;
|
||||
actionPredicates = [] as ActionPredicateFn[];
|
||||
|
||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||
|
||||
@ -68,6 +69,37 @@ export class ActionManager {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
registerActionPredicate(predicate: ActionPredicateFn) {
|
||||
if (!this.actionPredicates.includes(predicate)) {
|
||||
this.actionPredicates.push(predicate);
|
||||
}
|
||||
}
|
||||
|
||||
filterActions(
|
||||
filter: ActionPredicateFn,
|
||||
opts?: {
|
||||
elements?: readonly ExcalidrawElement[];
|
||||
data?: Record<string, any>;
|
||||
},
|
||||
): Action[] {
|
||||
// For testing
|
||||
if (this === undefined) {
|
||||
return [];
|
||||
}
|
||||
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||
const appState = this.getAppState();
|
||||
const data = opts?.data;
|
||||
|
||||
const actions: Action[] = [];
|
||||
for (const key in this.actions) {
|
||||
const action = this.actions[key];
|
||||
if (filter(action, elements, appState, data)) {
|
||||
actions.push(action);
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
registerAction(action: Action) {
|
||||
this.actions[action.name] = action;
|
||||
}
|
||||
@ -84,7 +116,7 @@ export class ActionManager {
|
||||
(action) =>
|
||||
(action.name in canvasActions
|
||||
? canvasActions[action.name as keyof typeof canvasActions]
|
||||
: true) &&
|
||||
: this.isActionEnabled(action, { noPredicates: true })) &&
|
||||
action.keyTest &&
|
||||
action.keyTest(
|
||||
event,
|
||||
@ -135,7 +167,7 @@ export class ActionManager {
|
||||
/**
|
||||
* @param data additional data sent to the PanelComponent
|
||||
*/
|
||||
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
||||
renderAction = (name: Action["name"], data?: PanelComponentProps["data"]) => {
|
||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||
|
||||
if (
|
||||
@ -143,7 +175,7 @@ export class ActionManager {
|
||||
"PanelComponent" in this.actions[name] &&
|
||||
(name in canvasActions
|
||||
? canvasActions[name as keyof typeof canvasActions]
|
||||
: true)
|
||||
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
|
||||
) {
|
||||
const action = this.actions[name];
|
||||
const PanelComponent = action.PanelComponent!;
|
||||
@ -165,6 +197,7 @@ export class ActionManager {
|
||||
|
||||
return (
|
||||
<PanelComponent
|
||||
key={name}
|
||||
elements={this.getElementsIncludingDeleted()}
|
||||
appState={this.getAppState()}
|
||||
updateData={updateData}
|
||||
@ -178,13 +211,31 @@ export class ActionManager {
|
||||
return null;
|
||||
};
|
||||
|
||||
isActionEnabled = (action: Action) => {
|
||||
const elements = this.getElementsIncludingDeleted();
|
||||
isActionEnabled = (
|
||||
action: Action,
|
||||
opts?: {
|
||||
elements?: readonly ExcalidrawElement[];
|
||||
data?: Record<string, any>;
|
||||
noPredicates?: boolean;
|
||||
},
|
||||
): boolean => {
|
||||
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||
const appState = this.getAppState();
|
||||
const data = opts?.data;
|
||||
|
||||
return (
|
||||
!action.predicate ||
|
||||
action.predicate(elements, appState, this.app.props, this.app)
|
||||
);
|
||||
if (
|
||||
!opts?.noPredicates &&
|
||||
action.predicate &&
|
||||
!action.predicate(elements, appState, this.app.props, this.app, data)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let enabled = true;
|
||||
this.actionPredicates.forEach((fn) => {
|
||||
if (!fn(action, elements, appState, data)) {
|
||||
enabled = false;
|
||||
}
|
||||
});
|
||||
return enabled;
|
||||
};
|
||||
}
|
||||
|
@ -83,8 +83,23 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
||||
};
|
||||
|
||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||
const shortcuts = shortcutMap[name];
|
||||
export type CustomShortcutName = string;
|
||||
|
||||
let customShortcutMap: Record<CustomShortcutName, string[]> = {};
|
||||
|
||||
export const registerCustomShortcuts = (
|
||||
shortcuts: Record<CustomShortcutName, string[]>,
|
||||
) => {
|
||||
customShortcutMap = { ...customShortcutMap, ...shortcuts };
|
||||
};
|
||||
|
||||
export const getShortcutFromShortcutName = (
|
||||
name: ShortcutName | CustomShortcutName,
|
||||
) => {
|
||||
const shortcuts =
|
||||
name in customShortcutMap
|
||||
? customShortcutMap[name as CustomShortcutName]
|
||||
: shortcutMap[name as ShortcutName];
|
||||
// if multiple shortcuts available, take the first one
|
||||
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
||||
};
|
||||
|
@ -32,6 +32,15 @@ type ActionFn = (
|
||||
app: AppClassProperties,
|
||||
) => ActionResult | Promise<ActionResult>;
|
||||
|
||||
// Return `true` *unless* `Action` should be disabled
|
||||
// given `elements`, `appState`, and optionally `data`.
|
||||
export type ActionPredicateFn = (
|
||||
action: Action,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
data?: Record<string, any>,
|
||||
) => boolean;
|
||||
|
||||
export type UpdaterFn = (res: ActionResult) => void;
|
||||
export type ActionFilterFn = (action: Action) => void;
|
||||
|
||||
@ -135,7 +144,7 @@ export type PanelComponentProps = {
|
||||
};
|
||||
|
||||
export interface Action {
|
||||
name: ActionName;
|
||||
name: string;
|
||||
PanelComponent?: React.FC<PanelComponentProps>;
|
||||
perform: ActionFn;
|
||||
keyPriority?: number;
|
||||
@ -157,6 +166,7 @@ export interface Action {
|
||||
appState: AppState,
|
||||
appProps: ExcalidrawProps,
|
||||
app: AppClassProperties,
|
||||
data?: Record<string, any>,
|
||||
) => boolean;
|
||||
checked?: (appState: Readonly<AppState>) => boolean;
|
||||
trackEvent:
|
||||
|
@ -146,6 +146,8 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
editingGroupId: { browser: true, export: false, server: false },
|
||||
editingLinearElement: { browser: false, export: false, server: false },
|
||||
activeTool: { browser: true, export: false, server: false },
|
||||
activeSubtypes: { browser: true, export: false, server: false },
|
||||
customData: { browser: true, export: false, server: false },
|
||||
penMode: { browser: true, export: false, server: false },
|
||||
penDetected: { browser: true, export: false, server: false },
|
||||
errorMessage: { browser: false, export: false, server: false },
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||
import { NonDeletedExcalidrawElement } from "./element/types";
|
||||
import { randomId } from "./random";
|
||||
import { AppState } from "./types";
|
||||
import { selectSubtype } from "./element/subtypes";
|
||||
|
||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||
|
||||
@ -23,6 +25,8 @@ export interface Spreadsheet {
|
||||
title: string | null;
|
||||
labels: string[] | null;
|
||||
values: number[];
|
||||
activeSubtypes?: AppState["activeSubtypes"];
|
||||
customData?: AppState["customData"];
|
||||
}
|
||||
|
||||
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
||||
@ -193,13 +197,17 @@ const chartXLabels = (
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
): ChartElements => {
|
||||
const custom = selectSubtype(spreadsheet, "text");
|
||||
return (
|
||||
spreadsheet.labels?.map((label, index) => {
|
||||
return newTextElement({
|
||||
groupIds: [groupId],
|
||||
backgroundColor,
|
||||
...commonProps,
|
||||
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
||||
text:
|
||||
label.length > 8 && custom.subtype === undefined
|
||||
? `${label.slice(0, 5)}...`
|
||||
: label,
|
||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||
y: y + BAR_GAP / 2,
|
||||
width: BAR_WIDTH,
|
||||
@ -207,6 +215,7 @@ const chartXLabels = (
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
verticalAlign: "top",
|
||||
...custom,
|
||||
});
|
||||
}) || []
|
||||
);
|
||||
@ -227,6 +236,7 @@ const chartYLabels = (
|
||||
y: y - BAR_GAP,
|
||||
text: "0",
|
||||
textAlign: "right",
|
||||
...selectSubtype(spreadsheet, "text"),
|
||||
});
|
||||
|
||||
const maxYLabel = newTextElement({
|
||||
@ -237,6 +247,7 @@ const chartYLabels = (
|
||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||
text: Math.max(...spreadsheet.values).toLocaleString(),
|
||||
textAlign: "right",
|
||||
...selectSubtype(spreadsheet, "text"),
|
||||
});
|
||||
|
||||
return [minYLabel, maxYLabel];
|
||||
@ -264,6 +275,7 @@ const chartLines = (
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
});
|
||||
|
||||
const yLine = newLinearElement({
|
||||
@ -280,6 +292,7 @@ const chartLines = (
|
||||
[0, 0],
|
||||
[0, -chartHeight],
|
||||
],
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
});
|
||||
|
||||
const maxLine = newLinearElement({
|
||||
@ -298,6 +311,7 @@ const chartLines = (
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
});
|
||||
|
||||
return [xLine, yLine, maxLine];
|
||||
@ -324,6 +338,7 @@ const chartBaseElements = (
|
||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||
roundness: null,
|
||||
textAlign: "center",
|
||||
...selectSubtype(spreadsheet, "text"),
|
||||
})
|
||||
: null;
|
||||
|
||||
@ -340,6 +355,7 @@ const chartBaseElements = (
|
||||
strokeColor: COLOR_PALETTE.black,
|
||||
fillStyle: "solid",
|
||||
opacity: 6,
|
||||
...selectSubtype(spreadsheet, "rectangle"),
|
||||
})
|
||||
: null;
|
||||
|
||||
@ -372,6 +388,7 @@ const chartTypeBar = (
|
||||
y: y - barHeight - BAR_GAP,
|
||||
width: BAR_WIDTH,
|
||||
height: barHeight,
|
||||
...selectSubtype(spreadsheet, "rectangle"),
|
||||
});
|
||||
});
|
||||
|
||||
@ -424,6 +441,7 @@ const chartTypeLine = (
|
||||
width: maxX - minX,
|
||||
strokeWidth: 2,
|
||||
points: points as any,
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
});
|
||||
|
||||
const dots = spreadsheet.values.map((value, index) => {
|
||||
@ -440,6 +458,7 @@ const chartTypeLine = (
|
||||
y: y + cy - BAR_GAP * 2,
|
||||
width: BAR_GAP,
|
||||
height: BAR_GAP,
|
||||
...selectSubtype(spreadsheet, "ellipse"),
|
||||
});
|
||||
});
|
||||
|
||||
@ -462,6 +481,7 @@ const chartTypeLine = (
|
||||
[0, 0],
|
||||
[0, cy],
|
||||
],
|
||||
...selectSubtype(spreadsheet, "line"),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./element/types";
|
||||
import { BinaryFiles } from "./types";
|
||||
import { AppState, BinaryFiles } from "./types";
|
||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||
@ -167,6 +167,7 @@ export const getSystemClipboard = async (
|
||||
export const parseClipboard = async (
|
||||
event: ClipboardEvent | null,
|
||||
isPlainPaste = false,
|
||||
appState?: AppState,
|
||||
): Promise<ClipboardData> => {
|
||||
const systemClipboard = await getSystemClipboard(event);
|
||||
|
||||
@ -186,6 +187,10 @@ export const parseClipboard = async (
|
||||
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard);
|
||||
|
||||
if (spreadsheetResult) {
|
||||
if ("spreadsheet" in spreadsheetResult) {
|
||||
spreadsheetResult.spreadsheet.activeSubtypes = appState?.activeSubtypes;
|
||||
spreadsheetResult.spreadsheet.customData = appState?.customData;
|
||||
}
|
||||
return spreadsheetResult;
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
} from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { SubtypeShapeActions, SubtypeToggles } from "./Subtypes";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { hasBoundTextElement } from "../element/typeChecks";
|
||||
@ -100,6 +101,7 @@ export const SelectedShapeActions = ({
|
||||
{showChangeBackgroundIcons && (
|
||||
<div>{renderAction("changeBackgroundColor")}</div>
|
||||
)}
|
||||
<SubtypeShapeActions elements={targetElements} />
|
||||
{showFillIcons && renderAction("changeFillStyle")}
|
||||
|
||||
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||
@ -400,6 +402,7 @@ export const ShapesSwitcher = ({
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<SubtypeToggles />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -270,6 +270,16 @@ import {
|
||||
import LayerUI from "./LayerUI";
|
||||
import { Toast } from "./Toast";
|
||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||
import {
|
||||
SubtypeLoadedCb,
|
||||
SubtypeRecord,
|
||||
SubtypePrepFn,
|
||||
checkRefreshOnSubtypeLoad,
|
||||
isSubtypeAction,
|
||||
prepareSubtype,
|
||||
selectSubtype,
|
||||
subtypeActionPredicate,
|
||||
} from "../element/subtypes";
|
||||
import {
|
||||
dataURLToFile,
|
||||
generateIdFromFile,
|
||||
@ -509,6 +519,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.id = nanoid();
|
||||
|
||||
this.library = new Library(this);
|
||||
this.actionManager = new ActionManager(
|
||||
this.syncActionResult,
|
||||
() => this.state,
|
||||
() => this.scene.getElementsIncludingDeleted(),
|
||||
this,
|
||||
);
|
||||
this.scene = new Scene();
|
||||
|
||||
this.canvas = document.createElement("canvas");
|
||||
@ -535,6 +551,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
getSceneElements: this.getSceneElements,
|
||||
getAppState: () => this.state,
|
||||
getFiles: () => this.files,
|
||||
actionManager: this.actionManager,
|
||||
addSubtype: this.addSubtype,
|
||||
refresh: this.refresh,
|
||||
setToast: this.setToast,
|
||||
id: this.id,
|
||||
@ -562,16 +580,27 @@ class App extends React.Component<AppProps, AppState> {
|
||||
onSceneUpdated: this.onSceneUpdated,
|
||||
});
|
||||
this.history = new History();
|
||||
this.actionManager = new ActionManager(
|
||||
this.syncActionResult,
|
||||
() => this.state,
|
||||
() => this.scene.getElementsIncludingDeleted(),
|
||||
this,
|
||||
);
|
||||
this.actionManager.registerAll(actions);
|
||||
|
||||
this.actionManager.registerAction(createUndoAction(this.history));
|
||||
this.actionManager.registerAction(createRedoAction(this.history));
|
||||
this.actionManager.registerActionPredicate(subtypeActionPredicate);
|
||||
}
|
||||
|
||||
private addSubtype(record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) {
|
||||
const subtypeLoadedCb: SubtypeLoadedCb = (hasSubtype) => {
|
||||
const elements = this.getSceneElementsIncludingDeleted();
|
||||
// If there are any elements of the just-registered subtype,
|
||||
// refresh the scene to re-render each such element.
|
||||
if (checkRefreshOnSubtypeLoad(hasSubtype, elements)) {
|
||||
this.refresh();
|
||||
}
|
||||
};
|
||||
const prep = prepareSubtype(record, subtypePrepFn, subtypeLoadedCb);
|
||||
if (prep.actions) {
|
||||
this.actionManager.registerAll(prep.actions);
|
||||
}
|
||||
return prep;
|
||||
}
|
||||
|
||||
private onWindowMessage(event: MessageEvent) {
|
||||
@ -2159,7 +2188,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// (something something security)
|
||||
let file = event?.clipboardData?.files[0];
|
||||
|
||||
const data = await parseClipboard(event, isPlainPaste);
|
||||
const data = await parseClipboard(event, isPlainPaste, this.state);
|
||||
if (!file && data.text && !isPlainPaste) {
|
||||
const string = data.text.trim();
|
||||
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
||||
@ -2389,6 +2418,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
fontFamily: this.state.currentItemFontFamily,
|
||||
textAlign: this.state.currentItemTextAlign,
|
||||
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
||||
...selectSubtype(this.state, "text"),
|
||||
locked: false,
|
||||
};
|
||||
|
||||
@ -3525,6 +3555,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
verticalAlign: parentCenterPosition
|
||||
? VERTICAL_ALIGN.MIDDLE
|
||||
: DEFAULT_VERTICAL_ALIGN,
|
||||
...selectSubtype(this.state, "text"),
|
||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||
groupIds: container?.groupIds ?? [],
|
||||
lineHeight,
|
||||
@ -5378,6 +5409,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
roughness: this.state.currentItemRoughness,
|
||||
roundness: null,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
...selectSubtype(this.state, "image"),
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
@ -5478,6 +5510,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: null,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
...selectSubtype(this.state, elementType),
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
@ -5554,6 +5587,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness: this.getCurrentItemRoundness(elementType),
|
||||
...selectSubtype(this.state, elementType),
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
} as const;
|
||||
@ -7874,6 +7908,29 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
private getContextMenuItems = (
|
||||
type: "canvas" | "element",
|
||||
): ContextMenuItems => {
|
||||
const subtype: ContextMenuItems = [];
|
||||
this.actionManager
|
||||
.filterActions(isSubtypeAction)
|
||||
.forEach(
|
||||
(action) =>
|
||||
this.actionManager.isActionEnabled(action, { data: {} }) &&
|
||||
subtype.push(action),
|
||||
);
|
||||
if (subtype.length > 0) {
|
||||
subtype.push(CONTEXT_MENU_SEPARATOR);
|
||||
}
|
||||
const standard: ContextMenuItems = this._getContextMenuItems(type).filter(
|
||||
(item) =>
|
||||
!item ||
|
||||
item === CONTEXT_MENU_SEPARATOR ||
|
||||
this.actionManager.isActionEnabled(item, { noPredicates: true }),
|
||||
);
|
||||
return [...subtype, ...standard];
|
||||
};
|
||||
|
||||
private _getContextMenuItems = (
|
||||
type: "canvas" | "element",
|
||||
): ContextMenuItems => {
|
||||
const options: ContextMenuItems = [];
|
||||
|
||||
|
@ -10,6 +10,12 @@ import { useApp } from "./App";
|
||||
import { Dialog } from "./Dialog";
|
||||
|
||||
import "./PasteChartDialog.scss";
|
||||
import { ensureSubtypesLoaded } from "../element/subtypes";
|
||||
import { isTextElement } from "../element";
|
||||
import {
|
||||
getContainerElement,
|
||||
redrawTextBoundingBox,
|
||||
} from "../element/textElement";
|
||||
|
||||
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||
|
||||
@ -25,41 +31,54 @@ const ChartPreviewBtn = (props: {
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!props.spreadsheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = renderSpreadsheet(
|
||||
props.chartType,
|
||||
props.spreadsheet,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
setChartElements(elements);
|
||||
let svg: SVGSVGElement;
|
||||
const previewNode = previewRef.current!;
|
||||
|
||||
(async () => {
|
||||
svg = await exportToSvg(
|
||||
elements,
|
||||
{
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: oc.white,
|
||||
},
|
||||
null, // files
|
||||
);
|
||||
svg.querySelector(".style-fonts")?.remove();
|
||||
previewNode.replaceChildren();
|
||||
previewNode.appendChild(svg);
|
||||
(async () => {
|
||||
let elements: ChartElements;
|
||||
await ensureSubtypesLoaded(
|
||||
props.spreadsheet?.activeSubtypes ?? [],
|
||||
() => {
|
||||
if (!props.spreadsheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.selected) {
|
||||
(previewNode.parentNode as HTMLDivElement).focus();
|
||||
}
|
||||
elements = renderSpreadsheet(
|
||||
props.chartType,
|
||||
props.spreadsheet,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
elements.forEach(
|
||||
(el) =>
|
||||
isTextElement(el) &&
|
||||
redrawTextBoundingBox(el, getContainerElement(el)),
|
||||
);
|
||||
setChartElements(elements);
|
||||
},
|
||||
).then(async () => {
|
||||
svg = await exportToSvg(
|
||||
elements,
|
||||
{
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: oc.white,
|
||||
},
|
||||
null, // files
|
||||
);
|
||||
svg.querySelector(".style-fonts")?.remove();
|
||||
previewNode.replaceChildren();
|
||||
previewNode.appendChild(svg);
|
||||
|
||||
if (props.selected) {
|
||||
(previewNode.parentNode as HTMLDivElement).focus();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
previewNode.replaceChildren();
|
||||
};
|
||||
})();
|
||||
|
||||
return () => {
|
||||
previewNode.replaceChildren();
|
||||
};
|
||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||
|
||||
return (
|
||||
|
173
src/components/Subtypes.tsx
Normal file
173
src/components/Subtypes.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||
import { t } from "../i18n";
|
||||
import { Action } from "../actions/types";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
Subtype,
|
||||
getSubtypeNames,
|
||||
hasAlwaysEnabledActions,
|
||||
isSubtypeAction,
|
||||
isValidSubtype,
|
||||
subtypeCollides,
|
||||
} from "../element/subtypes";
|
||||
import { ExcalidrawElement, Theme } from "../element/types";
|
||||
import {
|
||||
useExcalidrawActionManager,
|
||||
useExcalidrawContainer,
|
||||
useExcalidrawSetAppState,
|
||||
} from "./App";
|
||||
import { ContextMenuItems } from "./ContextMenu";
|
||||
|
||||
export const SubtypeButton = (
|
||||
subtype: Subtype,
|
||||
parentType: ExcalidrawElement["type"],
|
||||
icon: ({ theme }: { theme: Theme }) => JSX.Element,
|
||||
key?: string,
|
||||
) => {
|
||||
const title = key !== undefined ? ` - ${getShortcutKey(key)}` : "";
|
||||
const keyTest: Action["keyTest"] =
|
||||
key !== undefined ? (event) => event.code === `Key${key}` : undefined;
|
||||
const subtypeAction: Action = {
|
||||
name: subtype,
|
||||
trackEvent: false,
|
||||
predicate: (...rest) => rest[4]?.subtype === subtype,
|
||||
perform: (elements, appState) => {
|
||||
const inactive = !appState.activeSubtypes?.includes(subtype) ?? true;
|
||||
const activeSubtypes: Subtype[] = [];
|
||||
if (appState.activeSubtypes) {
|
||||
activeSubtypes.push(...appState.activeSubtypes);
|
||||
}
|
||||
let activated = false;
|
||||
if (inactive) {
|
||||
// Ensure `element.subtype` is well-defined
|
||||
if (!subtypeCollides(subtype, activeSubtypes)) {
|
||||
activeSubtypes.push(subtype);
|
||||
activated = true;
|
||||
}
|
||||
} else {
|
||||
// Can only be active if appState.activeSubtypes is defined
|
||||
// and contains subtype.
|
||||
activeSubtypes.splice(activeSubtypes.indexOf(subtype), 1);
|
||||
}
|
||||
const type =
|
||||
appState.activeTool.type !== "custom" &&
|
||||
isValidSubtype(subtype, appState.activeTool.type)
|
||||
? appState.activeTool.type
|
||||
: parentType;
|
||||
const activeTool = !inactive
|
||||
? appState.activeTool
|
||||
: updateActiveTool(appState, { type });
|
||||
const selectedElementIds = activated ? {} : appState.selectedElementIds;
|
||||
const selectedGroupIds = activated ? {} : appState.selectedGroupIds;
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
activeSubtypes,
|
||||
selectedElementIds,
|
||||
selectedGroupIds,
|
||||
activeTool,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
keyTest,
|
||||
PanelComponent: ({ elements, appState, updateData, data }) => (
|
||||
<button
|
||||
className={clsx("ToolIcon_type_button", "ToolIcon_type_button--show", {
|
||||
ToolIcon: true,
|
||||
"ToolIcon--selected":
|
||||
appState.activeSubtypes !== undefined &&
|
||||
appState.activeSubtypes.includes(subtype),
|
||||
"ToolIcon--plain": true,
|
||||
})}
|
||||
title={`${t(`toolBar.${subtype}`)}${title}`}
|
||||
aria-label={t(`toolBar.${subtype}`)}
|
||||
onClick={() => {
|
||||
updateData(null);
|
||||
}}
|
||||
onContextMenu={
|
||||
data && "onContextMenu" in data
|
||||
? (event: React.MouseEvent) => {
|
||||
if (
|
||||
appState.activeSubtypes === undefined ||
|
||||
(appState.activeSubtypes !== undefined &&
|
||||
!appState.activeSubtypes.includes(subtype))
|
||||
) {
|
||||
updateData(null);
|
||||
}
|
||||
data.onContextMenu(event, subtype);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{
|
||||
<div className="ToolIcon__icon" aria-hidden="true">
|
||||
{icon.call(this, { theme: appState.theme })}
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
),
|
||||
};
|
||||
if (key === "") {
|
||||
delete subtypeAction.keyTest;
|
||||
}
|
||||
return subtypeAction;
|
||||
};
|
||||
|
||||
export const SubtypeToggles = () => {
|
||||
const am = useExcalidrawActionManager();
|
||||
const { container } = useExcalidrawContainer();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
|
||||
const onContextMenu = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
subtype: string,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { top: offsetTop, left: offsetLeft } =
|
||||
container!.getBoundingClientRect();
|
||||
const left = event.clientX - offsetLeft;
|
||||
const top = event.clientY - offsetTop;
|
||||
|
||||
const items: ContextMenuItems = [];
|
||||
am.filterActions(isSubtypeAction).forEach(
|
||||
(action) =>
|
||||
am.isActionEnabled(action, { data: { subtype } }) && items.push(action),
|
||||
);
|
||||
setAppState({}, () => {
|
||||
setAppState({
|
||||
contextMenu: { top, left, items },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{getSubtypeNames().map((subtype) =>
|
||||
am.renderAction(
|
||||
subtype,
|
||||
hasAlwaysEnabledActions(subtype) ? { onContextMenu } : {},
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SubtypeToggles.displayName = "SubtypeToggles";
|
||||
|
||||
export const SubtypeShapeActions = (props: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
}) => {
|
||||
const am = useExcalidrawActionManager();
|
||||
return (
|
||||
<>
|
||||
{am
|
||||
.filterActions(isSubtypeAction, { elements: props.elements })
|
||||
.map((action) => am.renderAction(action.name))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SubtypeShapeActions.displayName = "SubtypeShapeActions";
|
@ -13,7 +13,7 @@ import clsx from "clsx";
|
||||
import { Theme } from "../element/types";
|
||||
import { THEME } from "../constants";
|
||||
|
||||
const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
||||
export const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
||||
|
||||
const handlerColor = (theme: Theme) =>
|
||||
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
||||
|
@ -34,13 +34,14 @@ import {
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { bumpVersion } from "../element/mutateElement";
|
||||
import { getFontString, getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { isValidSubtype } from "../element/subtypes";
|
||||
import { MarkOptional, Mutable } from "../utility-types";
|
||||
import {
|
||||
detectLineHeight,
|
||||
getDefaultLineHeight,
|
||||
measureBaseline,
|
||||
measureTextElement,
|
||||
} from "../element/textElement";
|
||||
import { normalizeLink } from "./url";
|
||||
|
||||
@ -92,7 +93,8 @@ const repairBinding = (binding: PointBinding | null) => {
|
||||
};
|
||||
|
||||
const restoreElementWithProperties = <
|
||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
/** @deprecated */
|
||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||
@ -158,6 +160,9 @@ const restoreElementWithProperties = <
|
||||
locked: element.locked ?? false,
|
||||
};
|
||||
|
||||
if ("subtype" in element && isValidSubtype(element.subtype, base.type)) {
|
||||
base.subtype = element.subtype;
|
||||
}
|
||||
if ("customData" in element) {
|
||||
base.customData = element.customData;
|
||||
}
|
||||
@ -203,11 +208,7 @@ const restoreElement = (
|
||||
: // no element height likely means programmatic use, so default
|
||||
// to a fixed line height
|
||||
getDefaultLineHeight(element.fontFamily));
|
||||
const baseline = measureBaseline(
|
||||
element.text,
|
||||
getFontString(element),
|
||||
lineHeight,
|
||||
);
|
||||
const baseline = measureTextElement(element, { text }).baseline;
|
||||
element = restoreElementWithProperties(element, {
|
||||
fontSize,
|
||||
fontFamily,
|
||||
@ -528,6 +529,12 @@ export const restoreAppState = (
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
if ("activeSubtypes" in appState) {
|
||||
nextAppState.activeSubtypes = appState.activeSubtypes;
|
||||
}
|
||||
if ("customData" in appState) {
|
||||
nextAppState.customData = appState.customData;
|
||||
}
|
||||
return {
|
||||
...nextAppState,
|
||||
cursorButton: localAppState?.cursorButton || "up",
|
||||
|
@ -6,12 +6,23 @@ import { Point } from "../types";
|
||||
import { getUpdatedTimestamp } from "../utils";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { maybeGetSubtypeProps } from "./newElement";
|
||||
import { getSubtypeMethods } from "./subtypes";
|
||||
|
||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
"id" | "version" | "versionNonce"
|
||||
>;
|
||||
|
||||
const cleanUpdates = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
): ElementUpdate<TElement> => {
|
||||
const subtype = maybeGetSubtypeProps(element, element.type).subtype;
|
||||
const map = getSubtypeMethods(subtype);
|
||||
return map?.clean ? (map.clean(updates) as typeof updates) : updates;
|
||||
};
|
||||
|
||||
// This function tracks updates of text elements for the purposes for collaboration.
|
||||
// The version is used to compare updates when more than one user is working in
|
||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||
@ -22,6 +33,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
informMutation = true,
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
let increment = false;
|
||||
const oldUpdates = cleanUpdates(element, updates);
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
@ -70,6 +83,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
}
|
||||
}
|
||||
if (!didChangePoints) {
|
||||
key in oldUpdates && (increment = true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -77,6 +91,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
|
||||
(element as any)[key] = value;
|
||||
didChange = true;
|
||||
key in oldUpdates && (increment = true);
|
||||
}
|
||||
}
|
||||
if (!didChange) {
|
||||
@ -92,9 +107,11 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
ShapeCache.delete(element);
|
||||
}
|
||||
|
||||
element.version++;
|
||||
element.versionNonce = randomInteger();
|
||||
element.updated = getUpdatedTimestamp();
|
||||
if (increment) {
|
||||
element.version++;
|
||||
element.versionNonce = randomInteger();
|
||||
element.updated = getUpdatedTimestamp();
|
||||
}
|
||||
|
||||
if (informMutation) {
|
||||
Scene.getScene(element)?.informMutation();
|
||||
@ -108,6 +125,8 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
updates: ElementUpdate<TElement>,
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
let increment = false;
|
||||
const oldUpdates = cleanUpdates(element, updates);
|
||||
for (const key in updates) {
|
||||
const value = (updates as any)[key];
|
||||
if (typeof value !== "undefined") {
|
||||
@ -119,6 +138,7 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
continue;
|
||||
}
|
||||
didChange = true;
|
||||
key in oldUpdates && (increment = true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +146,9 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
return element;
|
||||
}
|
||||
|
||||
if (!increment) {
|
||||
return { ...element, ...updates };
|
||||
}
|
||||
return {
|
||||
...element,
|
||||
...updates,
|
||||
|
@ -15,12 +15,7 @@ import {
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
arrayToMap,
|
||||
getFontString,
|
||||
getUpdatedTimestamp,
|
||||
isTestEnv,
|
||||
} from "../utils";
|
||||
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||
import { randomInteger, randomId } from "../random";
|
||||
import { bumpVersion, newElementWith } from "./mutateElement";
|
||||
import { getNewGroupIdsForDuplication } from "../groups";
|
||||
@ -30,9 +25,9 @@ import { adjustXYWithRotation } from "../math";
|
||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||
import {
|
||||
getContainerElement,
|
||||
measureText,
|
||||
measureTextElement,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
wrapTextElement,
|
||||
getBoundTextMaxWidth,
|
||||
getDefaultLineHeight,
|
||||
} from "./textElement";
|
||||
@ -45,6 +40,30 @@ import {
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||
import { getSubtypeMethods, isValidSubtype } from "./subtypes";
|
||||
|
||||
export const maybeGetSubtypeProps = (
|
||||
obj: {
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
},
|
||||
type: ExcalidrawElement["type"],
|
||||
) => {
|
||||
const data: typeof obj = {};
|
||||
if ("subtype" in obj) {
|
||||
data.subtype = obj.subtype;
|
||||
}
|
||||
if ("customData" in obj) {
|
||||
data.customData = obj.customData;
|
||||
}
|
||||
if ("subtype" in data && !isValidSubtype(data.subtype, type)) {
|
||||
delete data.subtype;
|
||||
}
|
||||
if (!("subtype" in data) && "customData" in data) {
|
||||
delete data.customData;
|
||||
}
|
||||
return data as typeof obj;
|
||||
};
|
||||
|
||||
export type ElementConstructorOpts = MarkOptional<
|
||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||
@ -58,6 +77,8 @@ export type ElementConstructorOpts = MarkOptional<
|
||||
| "version"
|
||||
| "versionNonce"
|
||||
| "link"
|
||||
| "subtype"
|
||||
| "customData"
|
||||
| "strokeStyle"
|
||||
| "fillStyle"
|
||||
| "strokeColor"
|
||||
@ -93,8 +114,10 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
||||
...rest
|
||||
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
||||
) => {
|
||||
const { subtype, customData } = rest;
|
||||
// assign type to guard against excess properties
|
||||
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
||||
...maybeGetSubtypeProps({ subtype, customData }, type),
|
||||
id: rest.id || randomId(),
|
||||
type,
|
||||
x,
|
||||
@ -128,8 +151,11 @@ export const newElement = (
|
||||
opts: {
|
||||
type: ExcalidrawGenericElement["type"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawGenericElement> =>
|
||||
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||
): NonDeleted<ExcalidrawGenericElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||
};
|
||||
|
||||
export const newEmbeddableElement = (
|
||||
opts: {
|
||||
@ -196,10 +222,12 @@ export const newTextElement = (
|
||||
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
||||
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
|
||||
const text = normalizeText(opts.text);
|
||||
const metrics = measureText(
|
||||
text,
|
||||
getFontString({ fontFamily, fontSize }),
|
||||
lineHeight,
|
||||
const metrics = measureTextElement(
|
||||
{ ...opts, fontSize, fontFamily, lineHeight },
|
||||
{
|
||||
text,
|
||||
customData: opts.customData,
|
||||
},
|
||||
);
|
||||
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
||||
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
||||
@ -244,7 +272,9 @@ const getAdjustedDimensions = (
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextBaseline,
|
||||
} = measureText(nextText, getFontString(element), element.lineHeight);
|
||||
} = measureTextElement(element, {
|
||||
text: nextText,
|
||||
});
|
||||
const { textAlign, verticalAlign } = element;
|
||||
let x: number;
|
||||
let y: number;
|
||||
@ -253,11 +283,7 @@ const getAdjustedDimensions = (
|
||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||
!element.containerId
|
||||
) {
|
||||
const prevMetrics = measureText(
|
||||
element.text,
|
||||
getFontString(element),
|
||||
element.lineHeight,
|
||||
);
|
||||
const prevMetrics = measureTextElement(element);
|
||||
const offsets = getTextElementPositionOffsets(element, {
|
||||
width: nextWidth - prevMetrics.width,
|
||||
height: nextHeight - prevMetrics.height,
|
||||
@ -313,11 +339,9 @@ export const refreshTextDimensions = (
|
||||
}
|
||||
const container = getContainerElement(textElement);
|
||||
if (container) {
|
||||
text = wrapText(
|
||||
text = wrapTextElement(textElement, getBoundTextMaxWidth(container), {
|
||||
text,
|
||||
getFontString(textElement),
|
||||
getBoundTextMaxWidth(container),
|
||||
);
|
||||
});
|
||||
}
|
||||
const dimensions = getAdjustedDimensions(textElement, text);
|
||||
return { text, ...dimensions };
|
||||
@ -349,6 +373,8 @@ export const newFreeDrawElement = (
|
||||
simulatePressure: boolean;
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
@ -366,6 +392,8 @@ export const newLinearElement = (
|
||||
points?: ExcalidrawLinearElement["points"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawLinearElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
@ -385,6 +413,8 @@ export const newImageElement = (
|
||||
scale?: ExcalidrawImageElement["scale"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawImageElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawImageElement>("image", opts),
|
||||
// in the future we'll support changing stroke color for some SVG elements,
|
||||
|
@ -51,7 +51,7 @@ import {
|
||||
handleBindTextResize,
|
||||
getBoundTextMaxWidth,
|
||||
getApproxMinLineHeight,
|
||||
measureText,
|
||||
measureTextElement,
|
||||
getBoundTextMaxHeight,
|
||||
} from "./textElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
@ -223,11 +223,7 @@ const measureFontSizeFromWidth = (
|
||||
if (nextFontSize < MIN_FONT_SIZE) {
|
||||
return null;
|
||||
}
|
||||
const metrics = measureText(
|
||||
element.text,
|
||||
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
|
||||
element.lineHeight,
|
||||
);
|
||||
const metrics = measureTextElement(element, { fontSize: nextFontSize });
|
||||
return {
|
||||
size: nextFontSize,
|
||||
baseline: metrics.baseline + (nextHeight - metrics.height),
|
||||
|
490
src/element/subtypes/index.ts
Normal file
490
src/element/subtypes/index.ts
Normal file
@ -0,0 +1,490 @@
|
||||
import { useEffect } from "react";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement, NonDeleted } from "../types";
|
||||
import { getNonDeletedElements } from "../";
|
||||
import { getSelectedElements } from "../../scene";
|
||||
import { AppState, ExcalidrawImperativeAPI } from "../../types";
|
||||
import { registerAuxLangData } from "../../i18n";
|
||||
|
||||
import { Action, ActionName, ActionPredicateFn } from "../../actions/types";
|
||||
import {
|
||||
CustomShortcutName,
|
||||
registerCustomShortcuts,
|
||||
} from "../../actions/shortcuts";
|
||||
import { register } from "../../actions/register";
|
||||
import { hasBoundTextElement, isTextElement } from "../typeChecks";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
redrawTextBoundingBox,
|
||||
} from "../textElement";
|
||||
import { ShapeCache } from "../../scene/ShapeCache";
|
||||
import Scene from "../../scene/Scene";
|
||||
|
||||
// Use "let" instead of "const" so we can dynamically add subtypes
|
||||
let subtypeNames: readonly Subtype[] = [];
|
||||
let parentTypeMap: readonly {
|
||||
subtype: Subtype;
|
||||
parentType: ExcalidrawElement["type"];
|
||||
}[] = [];
|
||||
let subtypeActionMap: readonly {
|
||||
subtype: Subtype;
|
||||
actions: readonly SubtypeActionName[];
|
||||
}[] = [];
|
||||
let disabledActionMap: readonly {
|
||||
subtype: Subtype;
|
||||
actions: readonly DisabledActionName[];
|
||||
}[] = [];
|
||||
let alwaysEnabledMap: readonly {
|
||||
subtype: Subtype;
|
||||
actions: readonly SubtypeActionName[];
|
||||
}[] = [];
|
||||
|
||||
export type SubtypeRecord = Readonly<{
|
||||
subtype: Subtype;
|
||||
parents: readonly ExcalidrawElement["type"][];
|
||||
actionNames?: readonly SubtypeActionName[];
|
||||
disabledNames?: readonly DisabledActionName[];
|
||||
shortcutMap?: Record<CustomShortcutName, string[]>;
|
||||
alwaysEnabledNames?: readonly SubtypeActionName[];
|
||||
}>;
|
||||
|
||||
// Subtype Names
|
||||
export type Subtype = Required<ExcalidrawElement>["subtype"];
|
||||
export const getSubtypeNames = (): readonly Subtype[] => {
|
||||
return subtypeNames;
|
||||
};
|
||||
export const isValidSubtype = (s: any, t: any): s is Subtype =>
|
||||
parentTypeMap.find(
|
||||
(val) => (val.subtype as any) === s && (val.parentType as any) === t,
|
||||
) !== undefined;
|
||||
const isSubtypeName = (s: any): s is Subtype => subtypeNames.includes(s);
|
||||
|
||||
// Subtype Actions
|
||||
|
||||
// Used for context menus in the shape chooser
|
||||
export const hasAlwaysEnabledActions = (s: any): boolean => {
|
||||
if (!isSubtypeName(s)) {
|
||||
return false;
|
||||
}
|
||||
return alwaysEnabledMap.some((value) => value.subtype === s);
|
||||
};
|
||||
|
||||
type SubtypeActionName = string;
|
||||
|
||||
const isSubtypeActionName = (s: any): s is SubtypeActionName =>
|
||||
subtypeActionMap.some((val) => val.actions.includes(s));
|
||||
|
||||
const addSubtypeAction = (action: Action) => {
|
||||
if (isSubtypeActionName(action.name) || isSubtypeName(action.name)) {
|
||||
register(action);
|
||||
}
|
||||
};
|
||||
|
||||
// Standard actions disabled by subtypes
|
||||
type DisabledActionName = ActionName;
|
||||
|
||||
const isDisabledActionName = (s: any): s is DisabledActionName =>
|
||||
disabledActionMap.some((val) => val.actions.includes(s));
|
||||
|
||||
// Is the `actionName` one of the subtype actions for `subtype`
|
||||
// (if `isAdded` is true) or one of the standard actions disabled
|
||||
// by `subtype` (if `isAdded` is false)?
|
||||
const isForSubtype = (
|
||||
subtype: ExcalidrawElement["subtype"],
|
||||
actionName: ActionName | SubtypeActionName,
|
||||
isAdded: boolean,
|
||||
) => {
|
||||
const actions = isAdded ? subtypeActionMap : disabledActionMap;
|
||||
const map = actions.find((value) => value.subtype === subtype);
|
||||
if (map) {
|
||||
return map.actions.includes(actionName);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isSubtypeAction: ActionPredicateFn = function (action) {
|
||||
return isSubtypeActionName(action.name) && !isSubtypeName(action.name);
|
||||
};
|
||||
|
||||
export const subtypeActionPredicate: ActionPredicateFn = function (
|
||||
action,
|
||||
elements,
|
||||
appState,
|
||||
) {
|
||||
// We always enable subtype actions. Also let through standard actions
|
||||
// which no subtypes might have disabled.
|
||||
if (
|
||||
isSubtypeName(action.name) ||
|
||||
(!isSubtypeActionName(action.name) && !isDisabledActionName(action.name))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
const chosen = appState.editingElement
|
||||
? [appState.editingElement, ...selectedElements]
|
||||
: selectedElements;
|
||||
// Now handle actions added by subtypes
|
||||
if (isSubtypeActionName(action.name)) {
|
||||
// Has any ExcalidrawElement enabled this actionName through having
|
||||
// its subtype?
|
||||
return (
|
||||
chosen.some((el) => {
|
||||
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
||||
return isForSubtype(e.subtype, action.name, true);
|
||||
}) ||
|
||||
// Or has any active subtype enabled this actionName?
|
||||
(appState.activeSubtypes !== undefined &&
|
||||
appState.activeSubtypes?.some((subtype) => {
|
||||
if (!isValidSubtype(subtype, appState.activeTool.type)) {
|
||||
return false;
|
||||
}
|
||||
return isForSubtype(subtype, action.name, true);
|
||||
})) ||
|
||||
alwaysEnabledMap.some((value) => {
|
||||
return value.actions.includes(action.name);
|
||||
})
|
||||
);
|
||||
}
|
||||
// Now handle standard actions disabled by subtypes
|
||||
if (isDisabledActionName(action.name)) {
|
||||
return (
|
||||
// Has every ExcalidrawElement not disabled this actionName?
|
||||
(chosen.every((el) => {
|
||||
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
||||
return !isForSubtype(e.subtype, action.name, false);
|
||||
}) &&
|
||||
// And has every active subtype not disabled this actionName?
|
||||
(appState.activeSubtypes === undefined ||
|
||||
appState.activeSubtypes?.every((subtype) => {
|
||||
if (!isValidSubtype(subtype, appState.activeTool.type)) {
|
||||
return true;
|
||||
}
|
||||
return !isForSubtype(subtype, action.name, false);
|
||||
}))) ||
|
||||
// Or can we find an ExcalidrawElement without a valid subtype
|
||||
// which would disable this action if it had a valid subtype?
|
||||
chosen.some((el) => {
|
||||
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
||||
return parentTypeMap.some(
|
||||
(value) =>
|
||||
value.parentType === e.type &&
|
||||
!isValidSubtype(e.subtype, e.type) &&
|
||||
isForSubtype(value.subtype, action.name, false),
|
||||
);
|
||||
}) ||
|
||||
chosen.some((el) => {
|
||||
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
||||
return (
|
||||
// Would the subtype of e by inself disable this action?
|
||||
isForSubtype(e.subtype, action.name, false) &&
|
||||
// Can we find an ExcalidrawElement which could have the same subtype
|
||||
// as e but whose subtype does not disable this action?
|
||||
chosen.some((el) => {
|
||||
const e2 = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
||||
return (
|
||||
// Does e have a valid subtype whose parent types include the
|
||||
// type of e2, and does the subtype of e2 not disable this action?
|
||||
parentTypeMap
|
||||
.filter((val) => val.subtype === e.subtype)
|
||||
.some((val) => val.parentType === e2.type) &&
|
||||
!isForSubtype(e2.subtype, action.name, false)
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
// Shouldn't happen
|
||||
return true;
|
||||
};
|
||||
|
||||
// Are any of the parent types of `subtype` shared by any subtype
|
||||
// in the array?
|
||||
export const subtypeCollides = (subtype: Subtype, subtypeArray: Subtype[]) => {
|
||||
const subtypeParents = parentTypeMap
|
||||
.filter((value) => value.subtype === subtype)
|
||||
.map((value) => value.parentType);
|
||||
const subtypeArrayParents = subtypeArray.flatMap((s) =>
|
||||
parentTypeMap
|
||||
.filter((value) => value.subtype === s)
|
||||
.map((value) => value.parentType),
|
||||
);
|
||||
return subtypeParents.some((t) => subtypeArrayParents.includes(t));
|
||||
};
|
||||
|
||||
// Subtype Methods
|
||||
export type SubtypeMethods = {
|
||||
clean: (
|
||||
updates: Omit<
|
||||
Partial<ExcalidrawElement>,
|
||||
"id" | "version" | "versionNonce"
|
||||
>,
|
||||
) => Omit<Partial<ExcalidrawElement>, "id" | "version" | "versionNonce">;
|
||||
getEditorStyle: (element: ExcalidrawTextElement) => Record<string, any>;
|
||||
ensureLoaded: (callback?: () => void) => Promise<void>;
|
||||
measureText: (
|
||||
element: Pick<
|
||||
ExcalidrawTextElement,
|
||||
| "subtype"
|
||||
| "customData"
|
||||
| "fontSize"
|
||||
| "fontFamily"
|
||||
| "text"
|
||||
| "lineHeight"
|
||||
>,
|
||||
next?: {
|
||||
fontSize?: number;
|
||||
text?: string;
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
},
|
||||
) => { width: number; height: number; baseline: number };
|
||||
render: (
|
||||
element: NonDeleted<ExcalidrawElement>,
|
||||
context: CanvasRenderingContext2D,
|
||||
) => void;
|
||||
renderSvg: (
|
||||
svgRoot: SVGElement,
|
||||
root: SVGElement,
|
||||
element: NonDeleted<ExcalidrawElement>,
|
||||
opt?: { offsetX?: number; offsetY?: number },
|
||||
) => void;
|
||||
wrapText: (
|
||||
element: Pick<
|
||||
ExcalidrawTextElement,
|
||||
| "subtype"
|
||||
| "customData"
|
||||
| "fontSize"
|
||||
| "fontFamily"
|
||||
| "originalText"
|
||||
| "lineHeight"
|
||||
>,
|
||||
containerWidth: number,
|
||||
next?: {
|
||||
fontSize?: number;
|
||||
text?: string;
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
},
|
||||
) => string;
|
||||
};
|
||||
|
||||
type MethodMap = { subtype: Subtype; methods: Partial<SubtypeMethods> };
|
||||
const methodMaps = [] as Array<MethodMap>;
|
||||
|
||||
// Use `getSubtypeMethods` to call subtype-specialized methods, like `render`.
|
||||
export const getSubtypeMethods = (
|
||||
subtype: Subtype | undefined,
|
||||
): Partial<SubtypeMethods> | undefined => {
|
||||
const map = methodMaps.find((method) => method.subtype === subtype);
|
||||
return map?.methods;
|
||||
};
|
||||
|
||||
export const addSubtypeMethods = (
|
||||
subtype: Subtype,
|
||||
methods: Partial<SubtypeMethods>,
|
||||
) => {
|
||||
if (!methodMaps.find((method) => method.subtype === subtype)) {
|
||||
methodMaps.push({ subtype, methods });
|
||||
}
|
||||
};
|
||||
|
||||
// For a given `ExcalidrawElement` type, return the active subtype
|
||||
// and associated customData (if any) from the AppState. Assume
|
||||
// only one subtype is active for a given `ExcalidrawElement` type
|
||||
// at any given time.
|
||||
export const selectSubtype = (
|
||||
appState: {
|
||||
activeSubtypes?: AppState["activeSubtypes"];
|
||||
customData?: AppState["customData"];
|
||||
},
|
||||
type: ExcalidrawElement["type"],
|
||||
): {
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
} => {
|
||||
if (appState.activeSubtypes === undefined) {
|
||||
return {};
|
||||
}
|
||||
const subtype = appState.activeSubtypes.find((subtype) =>
|
||||
isValidSubtype(subtype, type),
|
||||
);
|
||||
if (subtype === undefined) {
|
||||
return {};
|
||||
}
|
||||
if (appState.customData === undefined || !(subtype in appState.customData)) {
|
||||
return { subtype };
|
||||
}
|
||||
const customData = appState.customData[subtype];
|
||||
return { subtype, customData };
|
||||
};
|
||||
|
||||
// Callback to re-render subtyped `ExcalidrawElement`s after completing
|
||||
// async loading of the subtype.
|
||||
export type SubtypeLoadedCb = (hasSubtype: SubtypeCheckFn) => void;
|
||||
export type SubtypeCheckFn = (element: ExcalidrawElement) => boolean;
|
||||
|
||||
// Functions to prepare subtypes for use
|
||||
export type SubtypePrepFn = (
|
||||
addSubtypeAction: (action: Action) => void,
|
||||
addLangData: (
|
||||
fallbackLangData: Object,
|
||||
setLanguageAux: (langCode: string) => Promise<Object | undefined>,
|
||||
) => void,
|
||||
onSubtypeLoaded?: SubtypeLoadedCb,
|
||||
) => {
|
||||
actions: Action[];
|
||||
methods: Partial<SubtypeMethods>;
|
||||
};
|
||||
|
||||
// This is the main method to set up the subtype. The optional
|
||||
// `onSubtypeLoaded` callback may be used to re-render subtyped
|
||||
// `ExcalidrawElement`s after the subtype has finished async loading.
|
||||
// See the MathJax extension in `@excalidraw/extensions` for example.
|
||||
export const prepareSubtype = (
|
||||
record: SubtypeRecord,
|
||||
subtypePrepFn: SubtypePrepFn,
|
||||
onSubtypeLoaded?: SubtypeLoadedCb,
|
||||
): { actions: Action[] | null; methods: Partial<SubtypeMethods> } => {
|
||||
const map = getSubtypeMethods(record.subtype);
|
||||
if (map) {
|
||||
return { actions: null, methods: map };
|
||||
}
|
||||
|
||||
// Check for undefined/null subtypes and parentTypes
|
||||
if (
|
||||
record.subtype === undefined ||
|
||||
record.subtype === "" ||
|
||||
record.parents === undefined ||
|
||||
record.parents.length === 0
|
||||
) {
|
||||
return { actions: null, methods: {} };
|
||||
}
|
||||
|
||||
// Register the types
|
||||
const subtype = record.subtype;
|
||||
subtypeNames = [...subtypeNames, subtype];
|
||||
record.parents.forEach((parentType) => {
|
||||
parentTypeMap = [...parentTypeMap, { subtype, parentType }];
|
||||
});
|
||||
if (record.actionNames) {
|
||||
subtypeActionMap = [
|
||||
...subtypeActionMap,
|
||||
{ subtype, actions: record.actionNames },
|
||||
];
|
||||
}
|
||||
if (record.disabledNames) {
|
||||
disabledActionMap = [
|
||||
...disabledActionMap,
|
||||
{ subtype, actions: record.disabledNames },
|
||||
];
|
||||
}
|
||||
if (record.alwaysEnabledNames) {
|
||||
alwaysEnabledMap = [
|
||||
...alwaysEnabledMap,
|
||||
{ subtype, actions: record.alwaysEnabledNames },
|
||||
];
|
||||
}
|
||||
if (record.shortcutMap) {
|
||||
registerCustomShortcuts(record.shortcutMap);
|
||||
}
|
||||
|
||||
// Prepare the subtype
|
||||
const { actions, methods } = subtypePrepFn(
|
||||
addSubtypeAction,
|
||||
registerAuxLangData,
|
||||
onSubtypeLoaded,
|
||||
);
|
||||
|
||||
// Register the subtype's methods
|
||||
addSubtypeMethods(record.subtype, methods);
|
||||
return { actions, methods };
|
||||
};
|
||||
|
||||
// Ensure all subtypes are loaded before continuing, eg to
|
||||
// render SVG previews of new charts. Chart-relevant subtypes
|
||||
// include math equations in titles or non hand-drawn line styles.
|
||||
export const ensureSubtypesLoadedForElements = async (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
callback?: () => void,
|
||||
) => {
|
||||
// Only ensure the loading of subtypes which are actually needed.
|
||||
// We don't want to be held up by eg downloading the MathJax SVG fonts
|
||||
// if we don't actually need them yet.
|
||||
const subtypesUsed = [] as Subtype[];
|
||||
elements.forEach((el) => {
|
||||
if (
|
||||
"subtype" in el &&
|
||||
isValidSubtype(el.subtype, el.type) &&
|
||||
!subtypesUsed.includes(el.subtype)
|
||||
) {
|
||||
subtypesUsed.push(el.subtype);
|
||||
}
|
||||
});
|
||||
await ensureSubtypesLoaded(subtypesUsed, callback);
|
||||
};
|
||||
|
||||
export const ensureSubtypesLoaded = async (
|
||||
subtypes: Subtype[],
|
||||
callback?: () => void,
|
||||
) => {
|
||||
// Use a for loop so we can do `await map.ensureLoaded()`
|
||||
for (let i = 0; i < subtypes.length; i++) {
|
||||
const subtype = subtypes[i];
|
||||
// Should be defined if prepareSubtype() has run
|
||||
const map = getSubtypeMethods(subtype);
|
||||
if (map?.ensureLoaded) {
|
||||
await map.ensureLoaded();
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
// Call this method after finishing any async loading for
|
||||
// subtypes of ExcalidrawElement if the newly loaded code
|
||||
// would change the rendering.
|
||||
export const checkRefreshOnSubtypeLoad = (
|
||||
hasSubtype: SubtypeCheckFn,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) => {
|
||||
let refreshNeeded = false;
|
||||
const scenes: Scene[] = [];
|
||||
getNonDeletedElements(elements).forEach((element) => {
|
||||
// If the element is of the subtype that was just
|
||||
// registered, update the element's dimensions, mark the
|
||||
// element for a re-render, and indicate the scene needs a refresh.
|
||||
if (hasSubtype(element)) {
|
||||
ShapeCache.delete(element);
|
||||
if (isTextElement(element)) {
|
||||
redrawTextBoundingBox(element, getContainerElement(element));
|
||||
}
|
||||
refreshNeeded = true;
|
||||
const scene = Scene.getScene(element);
|
||||
if (scene && !scenes.includes(scene)) {
|
||||
// Store in case we have multiple scenes
|
||||
scenes.push(scene);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Only inform each scene once
|
||||
scenes.forEach((scene) => scene.informMutation());
|
||||
return refreshNeeded;
|
||||
};
|
||||
|
||||
export const useSubtype = (
|
||||
api: ExcalidrawImperativeAPI | null,
|
||||
record: SubtypeRecord,
|
||||
subtypePrepFn: SubtypePrepFn,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (api) {
|
||||
const prep = api.addSubtype(record, subtypePrepFn);
|
||||
if (prep) {
|
||||
addSubtypeMethods(record.subtype, prep.methods);
|
||||
}
|
||||
}
|
||||
}, [api, record, subtypePrepFn]);
|
||||
};
|
13
src/element/subtypes/mathjax/icon.tsx
Normal file
13
src/element/subtypes/mathjax/icon.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Theme } from "../../../element/types";
|
||||
import { createIcon, iconFillColor } from "../../../components/icons";
|
||||
|
||||
// We inline font-awesome icons in order to save on js size rather than including the font awesome react library
|
||||
export const mathSubtypeIcon = ({ theme }: { theme: Theme }) =>
|
||||
createIcon(
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
// fa-square-root-variable-solid
|
||||
d="M289 24.2C292.5 10 305.3 0 320 0H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H345L239 487.8c-3.2 13-14.2 22.6-27.6 24s-26.1-5.5-32.1-17.5L76.2 288H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H96c12.1 0 23.2 6.8 28.6 17.7l73.3 146.6L289 24.2zM393.4 233.4c12.5-12.5 32.8-12.5 45.3 0L480 274.7l41.4-41.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3L525.3 320l41.4 41.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L480 365.3l-41.4 41.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L434.7 320l-41.4-41.4c-12.5-12.5-12.5-32.8 0-45.3z"
|
||||
/>,
|
||||
{ width: 576, height: 512, mirror: true, strokeWidth: 1.25 },
|
||||
);
|
1631
src/element/subtypes/mathjax/implementation.tsx
Normal file
1631
src/element/subtypes/mathjax/implementation.tsx
Normal file
File diff suppressed because it is too large
Load Diff
15
src/element/subtypes/mathjax/index.ts
Normal file
15
src/element/subtypes/mathjax/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ExcalidrawImperativeAPI } from "../../../types";
|
||||
import { useSubtype } from "../";
|
||||
import { getMathSubtypeRecord } from "./types";
|
||||
import { prepareMathSubtype } from "./implementation";
|
||||
|
||||
declare global {
|
||||
module SREfeature {
|
||||
function custom(locale: string): Promise<string>;
|
||||
}
|
||||
}
|
||||
|
||||
// The main hook to use the MathJax subtype
|
||||
export const useMathSubtype = (api: ExcalidrawImperativeAPI | null) => {
|
||||
useSubtype(api, getMathSubtypeRecord(), prepareMathSubtype);
|
||||
};
|
15
src/element/subtypes/mathjax/locales/en.json
Normal file
15
src/element/subtypes/mathjax/locales/en.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"labels": {
|
||||
"changeMathOnly": "Math display",
|
||||
"mathOnlyTrue": "Math only",
|
||||
"mathOnlyFalse": "Mixed text",
|
||||
"resetUseTex": "Reset math input type",
|
||||
"useTexTrueActive": "✔ Standard input",
|
||||
"useTexTrueInactive": "Standard input",
|
||||
"useTexFalseActive": "✔ Simplified input",
|
||||
"useTexFalseInactive": "Simplified input"
|
||||
},
|
||||
"toolBar": {
|
||||
"math": "Math"
|
||||
}
|
||||
}
|
77
src/element/subtypes/mathjax/tests/implementation.test.tsx
Normal file
77
src/element/subtypes/mathjax/tests/implementation.test.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { vi } from "vitest";
|
||||
import { render } from "../../../../tests/test-utils";
|
||||
import { API } from "../../../../tests/helpers/api";
|
||||
import { Excalidraw } from "../../../../packages/excalidraw/index";
|
||||
|
||||
import { measureTextElement } from "../../../textElement";
|
||||
import { ensureSubtypesLoaded } from "../../";
|
||||
import { getMathSubtypeRecord } from "../types";
|
||||
import { prepareMathSubtype } from "../implementation";
|
||||
|
||||
describe("mathjax loaded", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
API.addSubtype(getMathSubtypeRecord(), prepareMathSubtype);
|
||||
await ensureSubtypesLoaded(["math"]);
|
||||
});
|
||||
it("text-only measurements match", async () => {
|
||||
const text = "A quick brown fox jumps over the lazy dog.";
|
||||
const elements = [
|
||||
API.createElement({ type: "text", id: "A", text, subtype: "math" }),
|
||||
API.createElement({ type: "text", id: "B", text }),
|
||||
];
|
||||
const metrics1 = measureTextElement(elements[0]);
|
||||
const metrics2 = measureTextElement(elements[1]);
|
||||
expect(metrics1).toStrictEqual(metrics2);
|
||||
});
|
||||
it("minimum height remains", async () => {
|
||||
const elements = [
|
||||
API.createElement({ type: "text", id: "A", text: "a" }),
|
||||
API.createElement({
|
||||
type: "text",
|
||||
id: "B",
|
||||
text: "\\(\\alpha\\)",
|
||||
subtype: "math",
|
||||
customData: { useTex: true },
|
||||
}),
|
||||
API.createElement({
|
||||
type: "text",
|
||||
id: "C",
|
||||
text: "`beta`",
|
||||
subtype: "math",
|
||||
customData: { useTex: false },
|
||||
}),
|
||||
];
|
||||
const height = measureTextElement(elements[0]).height;
|
||||
const height1 = measureTextElement(elements[1]).height;
|
||||
const height2 = measureTextElement(elements[2]).height;
|
||||
expect(height).toEqual(height1);
|
||||
expect(height).toEqual(height2);
|
||||
});
|
||||
it("converts math to svgs", async () => {
|
||||
const svgDim = 42;
|
||||
vi.spyOn(SVGElement.prototype, "getBoundingClientRect").mockImplementation(
|
||||
() => new DOMRect(0, 0, svgDim, svgDim),
|
||||
);
|
||||
const elements = [];
|
||||
const type = "text";
|
||||
const subtype = "math";
|
||||
let text = "Math ";
|
||||
elements.push(API.createElement({ type, text }));
|
||||
text = "Math \\(\\alpha\\)";
|
||||
elements.push(
|
||||
API.createElement({ type, subtype, text, customData: { useTex: true } }),
|
||||
);
|
||||
text = "Math `beta`";
|
||||
elements.push(
|
||||
API.createElement({ type, subtype, text, customData: { useTex: false } }),
|
||||
);
|
||||
const metrics = {
|
||||
width: measureTextElement(elements[0]).width + svgDim,
|
||||
height: svgDim,
|
||||
baseline: 0,
|
||||
};
|
||||
expect(measureTextElement(elements[1])).toStrictEqual(metrics);
|
||||
expect(measureTextElement(elements[2])).toStrictEqual(metrics);
|
||||
});
|
||||
});
|
17
src/element/subtypes/mathjax/types.ts
Normal file
17
src/element/subtypes/mathjax/types.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { getShortcutKey } from "../../../utils";
|
||||
import { SubtypeRecord } from "../";
|
||||
|
||||
// Exports
|
||||
export const getMathSubtypeRecord = () => mathSubtype;
|
||||
|
||||
// Use `getMathSubtype` so we don't have to export this
|
||||
const mathSubtype: SubtypeRecord = {
|
||||
subtype: "math",
|
||||
parents: ["text"],
|
||||
actionNames: ["useTexTrue", "useTexFalse", "resetUseTex", "changeMathOnly"],
|
||||
disabledNames: ["changeFontFamily"],
|
||||
shortcutMap: {
|
||||
resetUseTex: [getShortcutKey("Shift+R")],
|
||||
},
|
||||
alwaysEnabledNames: ["useTexTrue", "useTexFalse"],
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { getSubtypeMethods, SubtypeMethods } from "./subtypes";
|
||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
@ -36,6 +37,30 @@ import {
|
||||
} from "./textWysiwyg";
|
||||
import { ExtractSetType } from "../utility-types";
|
||||
|
||||
export const measureTextElement = function (element, next) {
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.measureText) {
|
||||
return map.measureText(element, next);
|
||||
}
|
||||
|
||||
const fontSize = next?.fontSize ?? element.fontSize;
|
||||
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||
const text = next?.text ?? element.text;
|
||||
return measureText(text, font, element.lineHeight);
|
||||
} as SubtypeMethods["measureText"];
|
||||
|
||||
export const wrapTextElement = function (element, containerWidth, next) {
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.wrapText) {
|
||||
return map.wrapText(element, containerWidth, next);
|
||||
}
|
||||
|
||||
const fontSize = next?.fontSize ?? element.fontSize;
|
||||
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||
const text = next?.text ?? element.originalText;
|
||||
return wrapText(text, font, containerWidth);
|
||||
} as SubtypeMethods["wrapText"];
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
text
|
||||
@ -68,22 +93,24 @@ export const redrawTextBoundingBox = (
|
||||
|
||||
if (container) {
|
||||
maxWidth = getBoundTextMaxWidth(container, textElement);
|
||||
boundTextUpdates.text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
maxWidth,
|
||||
);
|
||||
boundTextUpdates.text = wrapTextElement(textElement, maxWidth);
|
||||
}
|
||||
const metrics = measureText(
|
||||
boundTextUpdates.text,
|
||||
getFontString(textElement),
|
||||
textElement.lineHeight,
|
||||
);
|
||||
const metrics = measureTextElement(textElement, {
|
||||
text: boundTextUpdates.text,
|
||||
});
|
||||
|
||||
boundTextUpdates.width = metrics.width;
|
||||
boundTextUpdates.height = metrics.height;
|
||||
boundTextUpdates.baseline = metrics.baseline;
|
||||
|
||||
// Maintain coordX for non left-aligned text in case the width has changed
|
||||
if (!container) {
|
||||
if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||
boundTextUpdates.x += textElement.width - metrics.width;
|
||||
} else if (textElement.textAlign === TEXT_ALIGN.CENTER) {
|
||||
boundTextUpdates.x += textElement.width / 2 - metrics.width / 2;
|
||||
}
|
||||
}
|
||||
if (container) {
|
||||
const maxContainerHeight = getBoundTextMaxHeight(
|
||||
container,
|
||||
@ -196,17 +223,9 @@ export const handleBindTextResize = (
|
||||
(transformHandleType !== "n" && transformHandleType !== "s")
|
||||
) {
|
||||
if (text) {
|
||||
text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
maxWidth,
|
||||
);
|
||||
text = wrapTextElement(textElement, maxWidth);
|
||||
}
|
||||
const metrics = measureText(
|
||||
text,
|
||||
getFontString(textElement),
|
||||
textElement.lineHeight,
|
||||
);
|
||||
const metrics = measureTextElement(textElement, { text });
|
||||
nextHeight = metrics.height;
|
||||
nextWidth = metrics.width;
|
||||
nextBaseLine = metrics.baseline;
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
getContainerElement,
|
||||
getTextElementAngle,
|
||||
getTextWidth,
|
||||
measureText,
|
||||
normalizeText,
|
||||
redrawTextBoundingBox,
|
||||
wrapText,
|
||||
@ -43,8 +44,10 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||
import App from "../components/App";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import { SubtypeMethods, getSubtypeMethods } from "./subtypes";
|
||||
|
||||
const getTransform = (
|
||||
offsetX: number,
|
||||
width: number,
|
||||
height: number,
|
||||
angle: number,
|
||||
@ -62,7 +65,8 @@ const getTransform = (
|
||||
if (height > maxHeight && zoom.value !== 1) {
|
||||
translateY = (maxHeight * (zoom.value - 1)) / 2;
|
||||
}
|
||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||
const offset = offsetX !== 0 ? ` translate(${offsetX}px, 0px)` : "";
|
||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)${offset}`;
|
||||
};
|
||||
|
||||
const originalContainerCache: {
|
||||
@ -97,6 +101,14 @@ export const getOriginalContainerHeightFromCache = (
|
||||
return originalContainerCache[id]?.height ?? null;
|
||||
};
|
||||
|
||||
const getEditorStyle = function (element) {
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.getEditorStyle) {
|
||||
return map.getEditorStyle(element);
|
||||
}
|
||||
return {};
|
||||
} as SubtypeMethods["getEditorStyle"];
|
||||
|
||||
export const textWysiwyg = ({
|
||||
id,
|
||||
onChange,
|
||||
@ -156,11 +168,24 @@ export const textWysiwyg = ({
|
||||
const container = getContainerElement(updatedTextElement);
|
||||
let maxWidth = updatedTextElement.width;
|
||||
|
||||
let maxHeight = updatedTextElement.height;
|
||||
let textElementWidth = updatedTextElement.width;
|
||||
// Editing metrics
|
||||
const eMetrics = measureText(
|
||||
container && updatedTextElement.containerId
|
||||
? wrapText(
|
||||
updatedTextElement.originalText,
|
||||
getFontString(updatedTextElement),
|
||||
getBoundTextMaxWidth(container),
|
||||
)
|
||||
: updatedTextElement.originalText,
|
||||
getFontString(updatedTextElement),
|
||||
updatedTextElement.lineHeight,
|
||||
);
|
||||
|
||||
let maxHeight = eMetrics.height;
|
||||
let textElementWidth = Math.max(updatedTextElement.width, eMetrics.width);
|
||||
// Set to element height by default since that's
|
||||
// what is going to be used for unbounded text
|
||||
const textElementHeight = updatedTextElement.height;
|
||||
const textElementHeight = Math.max(updatedTextElement.height, maxHeight);
|
||||
|
||||
if (container && updatedTextElement.containerId) {
|
||||
if (isArrowElement(container)) {
|
||||
@ -246,13 +271,35 @@ export const textWysiwyg = ({
|
||||
editable.selectionEnd = editable.value.length - diff;
|
||||
}
|
||||
|
||||
let transformWidth = updatedTextElement.width;
|
||||
if (!container) {
|
||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||
textElementWidth = Math.min(textElementWidth, maxWidth);
|
||||
} else {
|
||||
textElementWidth += 0.5;
|
||||
transformWidth += 0.5;
|
||||
}
|
||||
|
||||
// Horizontal offset in case updatedTextElement has a non-WYSIWYG subtype
|
||||
const offWidth = container
|
||||
? Math.min(
|
||||
0,
|
||||
updatedTextElement.width - Math.min(maxWidth, eMetrics.width),
|
||||
)
|
||||
: Math.min(maxWidth, updatedTextElement.width) -
|
||||
Math.min(maxWidth, eMetrics.width);
|
||||
const offsetX =
|
||||
textAlign === "right"
|
||||
? offWidth
|
||||
: textAlign === "center"
|
||||
? offWidth / 2
|
||||
: 0;
|
||||
const { width: w, height: h } = updatedTextElement;
|
||||
const transformOrigin =
|
||||
updatedTextElement.width !== eMetrics.width ||
|
||||
updatedTextElement.height !== eMetrics.height
|
||||
? { transformOrigin: `${w / 2}px ${h / 2}px` }
|
||||
: {};
|
||||
let lineHeight = updatedTextElement.lineHeight;
|
||||
|
||||
// In Safari the font size gets rounded off when rendering hence calculating the line height by rounding off font size
|
||||
@ -270,13 +317,15 @@ export const textWysiwyg = ({
|
||||
font: getFontString(updatedTextElement),
|
||||
// must be defined *after* font ¯\_(ツ)_/¯
|
||||
lineHeight,
|
||||
width: `${textElementWidth}px`,
|
||||
width: `${Math.min(textElementWidth, maxWidth)}px`,
|
||||
height: `${textElementHeight}px`,
|
||||
left: `${viewportX}px`,
|
||||
top: `${viewportY}px`,
|
||||
...transformOrigin,
|
||||
transform: getTransform(
|
||||
textElementWidth,
|
||||
textElementHeight,
|
||||
offsetX,
|
||||
transformWidth,
|
||||
updatedTextElement.height,
|
||||
getTextElementAngle(updatedTextElement),
|
||||
appState,
|
||||
maxWidth,
|
||||
@ -334,6 +383,7 @@ export const textWysiwyg = ({
|
||||
whiteSpace,
|
||||
overflowWrap: "break-word",
|
||||
boxSizing: "content-box",
|
||||
...getEditorStyle(element),
|
||||
});
|
||||
editable.value = element.originalText;
|
||||
updateWysiwygStyle();
|
||||
|
@ -65,6 +65,7 @@ type _ExcalidrawElementBase = Readonly<{
|
||||
updated: number;
|
||||
link: string | null;
|
||||
locked: boolean;
|
||||
subtype?: string;
|
||||
customData?: Record<string, any>;
|
||||
}>;
|
||||
|
||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -116,3 +116,5 @@ declare namespace jest {
|
||||
toBeNonNaNNumber(): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "mathjax-full/mjs/input/asciimath/legacy/MathJax";
|
||||
|
38
src/i18n.ts
38
src/i18n.ts
@ -87,6 +87,22 @@ if (import.meta.env.DEV) {
|
||||
let currentLang: Language = defaultLang;
|
||||
let currentLangData = {};
|
||||
|
||||
const auxCurrentLangData = Array<Object>();
|
||||
const auxFallbackLangData = Array<Object>();
|
||||
const auxSetLanguageFuncs =
|
||||
Array<(langCode: string) => Promise<Object | undefined>>();
|
||||
|
||||
export const registerAuxLangData = (
|
||||
fallbackLangData: Object,
|
||||
setLanguageAux: (langCode: string) => Promise<Object | undefined>,
|
||||
) => {
|
||||
if (auxFallbackLangData.includes(fallbackLangData)) {
|
||||
return;
|
||||
}
|
||||
auxFallbackLangData.push(fallbackLangData);
|
||||
auxSetLanguageFuncs.push(setLanguageAux);
|
||||
};
|
||||
|
||||
export const setLanguage = async (lang: Language) => {
|
||||
currentLang = lang;
|
||||
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
||||
@ -99,6 +115,17 @@ export const setLanguage = async (lang: Language) => {
|
||||
currentLangData = await import(
|
||||
/* webpackChunkName: "locales/[request]" */ `./locales/${currentLang.code}.json`
|
||||
);
|
||||
// Empty the auxCurrentLangData array
|
||||
while (auxCurrentLangData.length > 0) {
|
||||
auxCurrentLangData.pop();
|
||||
}
|
||||
// Fill the auxCurrentLangData array with each locale file found in auxLangDataRoots for this language
|
||||
auxSetLanguageFuncs.forEach(async (setLanguageFn) => {
|
||||
const condData = await setLanguageFn(currentLang.code);
|
||||
if (condData) {
|
||||
auxCurrentLangData.push(condData);
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to load language ${lang.code}:`, error.message);
|
||||
currentLangData = fallbackLangData;
|
||||
@ -125,7 +152,9 @@ const findPartsForData = (data: any, parts: string[]) => {
|
||||
};
|
||||
|
||||
export const t = (
|
||||
path: NestedKeyOf<typeof fallbackLangData>,
|
||||
path:
|
||||
| NestedKeyOf<typeof fallbackLangData>
|
||||
| `${NestedKeyOf<typeof fallbackLangData>}.${string}`,
|
||||
replacement?: { [key: string]: string | number } | null,
|
||||
fallback?: string,
|
||||
) => {
|
||||
@ -141,6 +170,13 @@ export const t = (
|
||||
findPartsForData(currentLangData, parts) ||
|
||||
findPartsForData(fallbackLangData, parts) ||
|
||||
fallback;
|
||||
const auxData = Array<Object>().concat(
|
||||
auxCurrentLangData,
|
||||
auxFallbackLangData,
|
||||
);
|
||||
for (let i = 0; i < auxData.length; i++) {
|
||||
translation = translation || findPartsForData(auxData[i], parts);
|
||||
}
|
||||
if (translation === undefined) {
|
||||
const errorMessage = `Can't find translation for ${path}`;
|
||||
// in production, don't blow up the app on a missing translation key
|
||||
|
@ -13,6 +13,7 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
## 0.16.0 (2023-09-19)
|
||||
|
||||
- Add a `subtype` attribute to `ExcalidrawElement` to allow self-contained extensions of any `ExcalidrawElement` type. Implement MathJax support on stem.excalidraw.com as a `math` subtype of `ExcalidrawTextElement`. Both standard Latex input and simplified AsciiMath input are supported. [#6037](https://github.com/excalidraw/excalidraw/pull/6037).
|
||||
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
|
||||
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
InteractiveCanvasAppState,
|
||||
} from "../types";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { getSubtypeMethods } from "../element/subtypes";
|
||||
import {
|
||||
BOUND_TEXT_PADDING,
|
||||
FRAME_STYLE,
|
||||
@ -264,6 +265,12 @@ const drawElementOnCanvas = (
|
||||
) => {
|
||||
context.globalAlpha =
|
||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.render) {
|
||||
map.render(element, context);
|
||||
context.globalAlpha = 1;
|
||||
return;
|
||||
}
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "embeddable":
|
||||
@ -897,6 +904,11 @@ export const renderElementToSvg = (
|
||||
root = anchorTag;
|
||||
}
|
||||
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.renderSvg) {
|
||||
map.renderSvg(svgRoot, root, element, { offsetX, offsetY });
|
||||
return;
|
||||
}
|
||||
const opacity =
|
||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||
|
||||
|
85
src/tests/customActions.test.tsx
Normal file
85
src/tests/customActions.test.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import { API } from "./helpers/api";
|
||||
import { render } from "./test-utils";
|
||||
import { Excalidraw } from "../packages/excalidraw/index";
|
||||
import {
|
||||
CustomShortcutName,
|
||||
getShortcutFromShortcutName,
|
||||
registerCustomShortcuts,
|
||||
} from "../actions/shortcuts";
|
||||
import { Action, ActionPredicateFn, ActionResult } from "../actions/types";
|
||||
import {
|
||||
actionChangeFontFamily,
|
||||
actionChangeFontSize,
|
||||
} from "../actions/actionProperties";
|
||||
import { isTextElement } from "../element";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
describe("regression tests", () => {
|
||||
it("should retrieve custom shortcuts", () => {
|
||||
const shortcuts: Record<CustomShortcutName, string[]> = {
|
||||
test: [getShortcutKey("CtrlOrCmd+1"), getShortcutKey("CtrlOrCmd+2")],
|
||||
};
|
||||
registerCustomShortcuts(shortcuts);
|
||||
expect(getShortcutFromShortcutName("test")).toBe("Ctrl+1");
|
||||
});
|
||||
|
||||
it("should apply universal action predicates", async () => {
|
||||
await render(<Excalidraw />);
|
||||
// Create the test elements
|
||||
const el1 = API.createElement({ type: "rectangle", id: "A", y: 0 });
|
||||
const el2 = API.createElement({ type: "rectangle", id: "B", y: 30 });
|
||||
const el3 = API.createElement({ type: "text", id: "C", y: 60 });
|
||||
const el12: ExcalidrawElement[] = [el1, el2];
|
||||
const el13: ExcalidrawElement[] = [el1, el3];
|
||||
const el23: ExcalidrawElement[] = [el2, el3];
|
||||
const el123: ExcalidrawElement[] = [el1, el2, el3];
|
||||
// Set up the custom Action enablers
|
||||
const enableName = "custom" as Action["name"];
|
||||
const enableAction: Action = {
|
||||
name: enableName,
|
||||
perform: (): ActionResult => {
|
||||
return {} as ActionResult;
|
||||
},
|
||||
trackEvent: false,
|
||||
};
|
||||
const enabler: ActionPredicateFn = function (action, elements) {
|
||||
if (action.name !== enableName || elements.some((el) => el.y === 30)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Set up the standard Action disablers
|
||||
const disabled1 = actionChangeFontFamily;
|
||||
const disabled2 = actionChangeFontSize;
|
||||
const disabler: ActionPredicateFn = function (action, elements) {
|
||||
if (
|
||||
action.name === disabled2.name &&
|
||||
elements.some((el) => el.y === 0 || isTextElement(el))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// Test the custom Action enablers
|
||||
const am = h.app.actionManager;
|
||||
am.registerActionPredicate(enabler);
|
||||
expect(am.isActionEnabled(enableAction, { elements: el12 })).toBe(true);
|
||||
expect(am.isActionEnabled(enableAction, { elements: el13 })).toBe(false);
|
||||
expect(am.isActionEnabled(enableAction, { elements: el23 })).toBe(true);
|
||||
expect(am.isActionEnabled(disabled1, { elements: el12 })).toBe(true);
|
||||
expect(am.isActionEnabled(disabled1, { elements: el13 })).toBe(true);
|
||||
expect(am.isActionEnabled(disabled1, { elements: el23 })).toBe(true);
|
||||
// Test the standard Action disablers
|
||||
am.registerActionPredicate(disabler);
|
||||
expect(am.isActionEnabled(disabled1, { elements: el123 })).toBe(true);
|
||||
expect(am.isActionEnabled(disabled2, { elements: [el1] })).toBe(false);
|
||||
expect(am.isActionEnabled(disabled2, { elements: [el2] })).toBe(true);
|
||||
expect(am.isActionEnabled(disabled2, { elements: [el3] })).toBe(false);
|
||||
expect(am.isActionEnabled(disabled2, { elements: el12 })).toBe(false);
|
||||
expect(am.isActionEnabled(disabled2, { elements: el23 })).toBe(false);
|
||||
expect(am.isActionEnabled(disabled2, { elements: el13 })).toBe(false);
|
||||
});
|
||||
});
|
@ -16,6 +16,16 @@ import util from "util";
|
||||
import path from "path";
|
||||
import { getMimeType } from "../../data/blob";
|
||||
import {
|
||||
SubtypeLoadedCb,
|
||||
SubtypePrepFn,
|
||||
SubtypeRecord,
|
||||
checkRefreshOnSubtypeLoad,
|
||||
prepareSubtype,
|
||||
selectSubtype,
|
||||
subtypeActionPredicate,
|
||||
} from "../../element/subtypes";
|
||||
import {
|
||||
maybeGetSubtypeProps,
|
||||
newEmbeddableElement,
|
||||
newFrameElement,
|
||||
newFreeDrawElement,
|
||||
@ -32,6 +42,26 @@ const readFile = util.promisify(fs.readFile);
|
||||
const { h } = window;
|
||||
|
||||
export class API {
|
||||
constructor() {
|
||||
h.app.actionManager.registerActionPredicate(subtypeActionPredicate);
|
||||
if (true) {
|
||||
// Call `prepareSubtype()` here for `@excalidraw/excalidraw`-specific subtypes
|
||||
}
|
||||
}
|
||||
|
||||
static addSubtype = (record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) => {
|
||||
const subtypeLoadedCb: SubtypeLoadedCb = (hasSubtype) => {
|
||||
if (checkRefreshOnSubtypeLoad(hasSubtype, h.elements)) {
|
||||
h.app.refresh();
|
||||
}
|
||||
};
|
||||
const prep = prepareSubtype(record, subtypePrepFn, subtypeLoadedCb);
|
||||
if (prep.actions) {
|
||||
h.app.actionManager.registerAll(prep.actions);
|
||||
}
|
||||
return prep;
|
||||
};
|
||||
|
||||
static setSelectedElements = (elements: ExcalidrawElement[]) => {
|
||||
h.setState({
|
||||
selectedElementIds: elements.reduce((acc, element) => {
|
||||
@ -112,6 +142,8 @@ export class API {
|
||||
verticalAlign?: T extends "text"
|
||||
? ExcalidrawTextElement["verticalAlign"]
|
||||
: never;
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
boundElements?: ExcalidrawGenericElement["boundElements"];
|
||||
containerId?: T extends "text"
|
||||
? ExcalidrawTextElement["containerId"]
|
||||
@ -140,6 +172,14 @@ export class API {
|
||||
|
||||
const appState = h?.state || getDefaultAppState();
|
||||
|
||||
const custom = maybeGetSubtypeProps(
|
||||
{
|
||||
subtype: rest.subtype ?? selectSubtype(appState, type)?.subtype,
|
||||
customData:
|
||||
rest.customData ?? selectSubtype(appState, type)?.customData,
|
||||
},
|
||||
type,
|
||||
);
|
||||
const base: Omit<
|
||||
ExcalidrawGenericElement,
|
||||
| "id"
|
||||
@ -155,6 +195,7 @@ export class API {
|
||||
| "link"
|
||||
| "updated"
|
||||
> = {
|
||||
...custom,
|
||||
x,
|
||||
y,
|
||||
angle: rest.angle ?? 0,
|
||||
|
7
src/tests/helpers/locales/en.json
Normal file
7
src/tests/helpers/locales/en.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"toolBar": {
|
||||
"test": "Test",
|
||||
"test2": "Test 2",
|
||||
"test3": "Test 3"
|
||||
}
|
||||
}
|
689
src/tests/subtypes.test.tsx
Normal file
689
src/tests/subtypes.test.tsx
Normal file
@ -0,0 +1,689 @@
|
||||
import { vi } from "vitest";
|
||||
import fallbackLangData from "./helpers/locales/en.json";
|
||||
import {
|
||||
SubtypeLoadedCb,
|
||||
SubtypeRecord,
|
||||
SubtypeMethods,
|
||||
SubtypePrepFn,
|
||||
addSubtypeMethods,
|
||||
ensureSubtypesLoadedForElements,
|
||||
getSubtypeMethods,
|
||||
getSubtypeNames,
|
||||
hasAlwaysEnabledActions,
|
||||
isValidSubtype,
|
||||
selectSubtype,
|
||||
subtypeCollides,
|
||||
} from "../element/subtypes";
|
||||
|
||||
import { render } from "./test-utils";
|
||||
import { API } from "./helpers/api";
|
||||
import { Excalidraw } from "../packages/excalidraw/index";
|
||||
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
FontString,
|
||||
Theme,
|
||||
} from "../element/types";
|
||||
import { createIcon, iconFillColor } from "../components/icons";
|
||||
import { SubtypeButton } from "../components/Subtypes";
|
||||
import { registerAuxLangData } from "../i18n";
|
||||
import { getFontString, getShortcutKey } from "../utils";
|
||||
import * as textElementUtils from "../element/textElement";
|
||||
import { isTextElement } from "../element";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import { Action, ActionName } from "../actions/types";
|
||||
import { AppState } from "../types";
|
||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||
import { actionChangeSloppiness } from "../actions";
|
||||
import { actionChangeRoundness } from "../actions/actionProperties";
|
||||
|
||||
const MW = 200;
|
||||
const TWIDTH = 200;
|
||||
const THEIGHT = 20;
|
||||
const TBASELINE = 0;
|
||||
const FONTSIZE = 20;
|
||||
const DBFONTSIZE = 40;
|
||||
const TRFONTSIZE = 60;
|
||||
|
||||
const getLangData = async (langCode: string): Promise<Object | undefined> => {
|
||||
try {
|
||||
const condData = await import(
|
||||
/* webpackChunkName: "locales/[request]" */ `./helpers/locales/${langCode}.json`
|
||||
);
|
||||
if (condData) {
|
||||
return condData;
|
||||
}
|
||||
} catch (e) {}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const testSubtypeIcon = ({ theme }: { theme: Theme }) =>
|
||||
createIcon(
|
||||
<path
|
||||
stroke={iconFillColor(theme)}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>,
|
||||
{ width: 40, height: 20, mirror: true },
|
||||
);
|
||||
|
||||
const TEST_ACTION = "testAction";
|
||||
const TEST_DISABLE1 = actionChangeSloppiness;
|
||||
const TEST_DISABLE3 = actionChangeRoundness;
|
||||
|
||||
const test1: SubtypeRecord = {
|
||||
subtype: "test",
|
||||
parents: ["line", "arrow", "rectangle", "diamond", "ellipse"],
|
||||
disabledNames: [TEST_DISABLE1.name as ActionName],
|
||||
actionNames: [TEST_ACTION],
|
||||
};
|
||||
|
||||
const testAction: Action = {
|
||||
name: TEST_ACTION,
|
||||
trackEvent: false,
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
elements,
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const test1Button = SubtypeButton(
|
||||
test1.subtype,
|
||||
test1.parents[0],
|
||||
testSubtypeIcon,
|
||||
);
|
||||
const test1NonParent = "text" as const;
|
||||
|
||||
const test2: SubtypeRecord = {
|
||||
subtype: "test2",
|
||||
parents: ["text"],
|
||||
};
|
||||
|
||||
const test2Button = SubtypeButton(
|
||||
test2.subtype,
|
||||
test2.parents[0],
|
||||
testSubtypeIcon,
|
||||
);
|
||||
|
||||
const test3: SubtypeRecord = {
|
||||
subtype: "test3",
|
||||
parents: ["text", "line"],
|
||||
shortcutMap: {
|
||||
testShortcut: [getShortcutKey("Shift+T")],
|
||||
},
|
||||
alwaysEnabledNames: ["test3Always"],
|
||||
disabledNames: [TEST_DISABLE3.name as ActionName],
|
||||
};
|
||||
|
||||
const test3Button = SubtypeButton(
|
||||
test3.subtype,
|
||||
test3.parents[0],
|
||||
testSubtypeIcon,
|
||||
);
|
||||
|
||||
const cleanTestElementUpdate = function (updates) {
|
||||
const oldUpdates = {};
|
||||
for (const key in updates) {
|
||||
if (key !== "roughness") {
|
||||
(oldUpdates as any)[key] = (updates as any)[key];
|
||||
}
|
||||
}
|
||||
(updates as any).roughness = 0;
|
||||
return oldUpdates;
|
||||
} as SubtypeMethods["clean"];
|
||||
|
||||
const prepareNullSubtype = function () {
|
||||
const methods = {} as SubtypeMethods;
|
||||
methods.clean = cleanTestElementUpdate;
|
||||
methods.measureText = measureTest2;
|
||||
methods.wrapText = wrapTest2;
|
||||
|
||||
const actions = [test1Button, test2Button, test3Button];
|
||||
return { actions, methods };
|
||||
} as SubtypePrepFn;
|
||||
|
||||
const prepareTest1Subtype = function (
|
||||
addSubtypeAction,
|
||||
addLangData,
|
||||
onSubtypeLoaded,
|
||||
) {
|
||||
const methods = {} as SubtypeMethods;
|
||||
methods.clean = cleanTestElementUpdate;
|
||||
|
||||
addLangData(fallbackLangData, getLangData);
|
||||
registerAuxLangData(fallbackLangData, getLangData);
|
||||
|
||||
const actions = [testAction, test1Button];
|
||||
actions.forEach((action) => addSubtypeAction(action));
|
||||
|
||||
return { actions, methods };
|
||||
} as SubtypePrepFn;
|
||||
|
||||
let test2Loaded = false;
|
||||
|
||||
const ensureLoadedTest2: SubtypeMethods["ensureLoaded"] = async (callback) => {
|
||||
test2Loaded = true;
|
||||
if (onTest2Loaded) {
|
||||
onTest2Loaded((el) => isTextElement(el) && el.subtype === test2.subtype);
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const measureTest2: SubtypeMethods["measureText"] = function (element, next) {
|
||||
const text = next?.text ?? element.text;
|
||||
const customData = next?.customData ?? {};
|
||||
const fontSize = customData.triple
|
||||
? TRFONTSIZE
|
||||
: next?.fontSize ?? element.fontSize;
|
||||
const fontFamily = element.fontFamily;
|
||||
const fontString = getFontString({ fontSize, fontFamily });
|
||||
const lineHeight = element.lineHeight;
|
||||
const metrics = textElementUtils.measureText(text, fontString, lineHeight);
|
||||
const width = test2Loaded
|
||||
? metrics.width * 2
|
||||
: Math.max(metrics.width - 10, 0);
|
||||
const height = test2Loaded
|
||||
? metrics.height * 2
|
||||
: Math.max(metrics.height - 5, 0);
|
||||
return { width, height, baseline: 1 };
|
||||
};
|
||||
|
||||
const wrapTest2: SubtypeMethods["wrapText"] = function (
|
||||
element,
|
||||
maxWidth,
|
||||
next,
|
||||
) {
|
||||
const text = next?.text ?? element.originalText;
|
||||
if (next?.customData && next?.customData.triple === true) {
|
||||
return `${text.split(" ").join("\n")}\nHELLO WORLD.`;
|
||||
}
|
||||
if (next?.fontSize === DBFONTSIZE) {
|
||||
return `${text.split(" ").join("\n")}\nHELLO World.`;
|
||||
}
|
||||
return `${text.split(" ").join("\n")}\nHello world.`;
|
||||
};
|
||||
|
||||
let onTest2Loaded: SubtypeLoadedCb | undefined;
|
||||
|
||||
const prepareTest2Subtype = function (
|
||||
addSubtypeAction,
|
||||
addLangData,
|
||||
onSubtypeLoaded,
|
||||
) {
|
||||
const methods = {
|
||||
ensureLoaded: ensureLoadedTest2,
|
||||
measureText: measureTest2,
|
||||
wrapText: wrapTest2,
|
||||
} as SubtypeMethods;
|
||||
|
||||
addLangData(fallbackLangData, getLangData);
|
||||
registerAuxLangData(fallbackLangData, getLangData);
|
||||
|
||||
const actions = [test2Button];
|
||||
actions.forEach((action) => addSubtypeAction(action));
|
||||
|
||||
onTest2Loaded = onSubtypeLoaded;
|
||||
|
||||
return { actions, methods };
|
||||
} as SubtypePrepFn;
|
||||
|
||||
const prepareTest3Subtype = function (
|
||||
addSubtypeAction,
|
||||
addLangData,
|
||||
onSubtypeLoaded,
|
||||
) {
|
||||
const methods = {} as SubtypeMethods;
|
||||
|
||||
addLangData(fallbackLangData, getLangData);
|
||||
registerAuxLangData(fallbackLangData, getLangData);
|
||||
|
||||
const actions = [test3Button];
|
||||
actions.forEach((action) => addSubtypeAction(action));
|
||||
|
||||
return { actions, methods };
|
||||
} as SubtypePrepFn;
|
||||
|
||||
const { h } = window;
|
||||
|
||||
describe("subtype registration", () => {
|
||||
it("should check for invalid subtype or parents", async () => {
|
||||
await render(<Excalidraw />, {});
|
||||
// Define invalid subtype records
|
||||
const null1 = {} as SubtypeRecord;
|
||||
const null2 = { subtype: "" } as SubtypeRecord;
|
||||
const null3 = { subtype: "null" } as SubtypeRecord;
|
||||
const null4 = { subtype: "null", parents: [] } as SubtypeRecord;
|
||||
// Try registering the invalid subtypes
|
||||
const prepN1 = API.addSubtype(null1, prepareNullSubtype);
|
||||
const prepN2 = API.addSubtype(null2, prepareNullSubtype);
|
||||
const prepN3 = API.addSubtype(null3, prepareNullSubtype);
|
||||
const prepN4 = API.addSubtype(null4, prepareNullSubtype);
|
||||
// Verify the guards in `prepareSubtype` worked
|
||||
expect(prepN1).toStrictEqual({ actions: null, methods: {} });
|
||||
expect(prepN2).toStrictEqual({ actions: null, methods: {} });
|
||||
expect(prepN3).toStrictEqual({ actions: null, methods: {} });
|
||||
expect(prepN4).toStrictEqual({ actions: null, methods: {} });
|
||||
});
|
||||
it("should return subtype actions and methods correctly", async () => {
|
||||
// Check initial registration works
|
||||
let prep1 = API.addSubtype(test1, prepareTest1Subtype);
|
||||
expect(prep1.actions).toStrictEqual([testAction, test1Button]);
|
||||
expect(prep1.methods).toStrictEqual({ clean: cleanTestElementUpdate });
|
||||
// Check repeat registration fails
|
||||
prep1 = API.addSubtype(test1, prepareNullSubtype);
|
||||
expect(prep1.actions).toBeNull();
|
||||
expect(prep1.methods).toStrictEqual({ clean: cleanTestElementUpdate });
|
||||
|
||||
// Check initial registration works
|
||||
let prep2 = API.addSubtype(test2, prepareTest2Subtype);
|
||||
expect(prep2.actions).toStrictEqual([test2Button]);
|
||||
expect(prep2.methods).toStrictEqual({
|
||||
ensureLoaded: ensureLoadedTest2,
|
||||
measureText: measureTest2,
|
||||
wrapText: wrapTest2,
|
||||
});
|
||||
// Check repeat registration fails
|
||||
prep2 = API.addSubtype(test2, prepareNullSubtype);
|
||||
expect(prep2.actions).toBeNull();
|
||||
expect(prep2.methods).toStrictEqual({
|
||||
ensureLoaded: ensureLoadedTest2,
|
||||
measureText: measureTest2,
|
||||
wrapText: wrapTest2,
|
||||
});
|
||||
|
||||
// Check initial registration works
|
||||
let prep3 = API.addSubtype(test3, prepareTest3Subtype);
|
||||
expect(prep3.actions).toStrictEqual([test3Button]);
|
||||
expect(prep3.methods).toStrictEqual({});
|
||||
// Check repeat registration fails
|
||||
prep3 = API.addSubtype(test3, prepareNullSubtype);
|
||||
expect(prep3.actions).toBeNull();
|
||||
expect(prep3.methods).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("subtypes", () => {
|
||||
it("should correctly register", async () => {
|
||||
const subtypes = getSubtypeNames();
|
||||
expect(subtypes).toContain(test1.subtype);
|
||||
expect(subtypes).toContain(test2.subtype);
|
||||
expect(subtypes).toContain(test3.subtype);
|
||||
});
|
||||
it("should return subtype methods", async () => {
|
||||
expect(getSubtypeMethods(undefined)).toBeUndefined();
|
||||
const test1Methods = getSubtypeMethods(test1.subtype);
|
||||
expect(test1Methods?.clean).toBeDefined();
|
||||
expect(test1Methods?.render).toBeUndefined();
|
||||
expect(test1Methods?.wrapText).toBeUndefined();
|
||||
expect(test1Methods?.renderSvg).toBeUndefined();
|
||||
expect(test1Methods?.measureText).toBeUndefined();
|
||||
expect(test1Methods?.ensureLoaded).toBeUndefined();
|
||||
});
|
||||
it("should not overwrite subtype methods", async () => {
|
||||
addSubtypeMethods(test1.subtype, {});
|
||||
addSubtypeMethods(test2.subtype, {});
|
||||
addSubtypeMethods(test3.subtype, { clean: cleanTestElementUpdate });
|
||||
const test1Methods = getSubtypeMethods(test1.subtype);
|
||||
expect(test1Methods?.clean).toBeDefined();
|
||||
const test2Methods = getSubtypeMethods(test2.subtype);
|
||||
expect(test2Methods?.measureText).toBeDefined();
|
||||
expect(test2Methods?.wrapText).toBeDefined();
|
||||
const test3Methods = getSubtypeMethods(test3.subtype);
|
||||
expect(test3Methods?.clean).toBeUndefined();
|
||||
});
|
||||
it("should register custom shortcuts", async () => {
|
||||
expect(getShortcutFromShortcutName("testShortcut")).toBe("Shift+T");
|
||||
});
|
||||
it("should correctly validate", async () => {
|
||||
test1.parents.forEach((p) => {
|
||||
expect(isValidSubtype(test1.subtype, p)).toBe(true);
|
||||
expect(isValidSubtype(undefined, p)).toBe(false);
|
||||
});
|
||||
expect(isValidSubtype(test1.subtype, test1NonParent)).toBe(false);
|
||||
expect(isValidSubtype(test1.subtype, undefined)).toBe(false);
|
||||
expect(isValidSubtype(undefined, undefined)).toBe(false);
|
||||
});
|
||||
it("should collide with themselves", async () => {
|
||||
expect(subtypeCollides(test1.subtype, [test1.subtype])).toBe(true);
|
||||
expect(subtypeCollides(test1.subtype, [test1.subtype, test2.subtype])).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
it("should not collide without type overlap", async () => {
|
||||
expect(subtypeCollides(test1.subtype, [test2.subtype])).toBe(false);
|
||||
});
|
||||
it("should collide with type overlap", async () => {
|
||||
expect(subtypeCollides(test1.subtype, [test3.subtype])).toBe(true);
|
||||
});
|
||||
it("should apply to ExcalidrawElements", async () => {
|
||||
const elements = [
|
||||
API.createElement({ type: "line", id: "A", subtype: test1.subtype }),
|
||||
API.createElement({ type: "arrow", id: "B", subtype: test1.subtype }),
|
||||
API.createElement({ type: "rectangle", id: "C", subtype: test1.subtype }),
|
||||
API.createElement({ type: "diamond", id: "D", subtype: test1.subtype }),
|
||||
API.createElement({ type: "ellipse", id: "E", subtype: test1.subtype }),
|
||||
];
|
||||
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||
elements.forEach((el) => expect(el.subtype).toBe(test1.subtype));
|
||||
});
|
||||
it("should enforce prop value restrictions", async () => {
|
||||
const elements = [
|
||||
API.createElement({
|
||||
type: "line",
|
||||
id: "A",
|
||||
subtype: test1.subtype,
|
||||
roughness: 1,
|
||||
}),
|
||||
API.createElement({ type: "line", id: "B", roughness: 1 }),
|
||||
];
|
||||
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||
elements.forEach((el) => {
|
||||
if (el.subtype === test1.subtype) {
|
||||
expect(el.roughness).toBe(0);
|
||||
} else {
|
||||
expect(el.roughness).toBe(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
it("should consider enforced prop values in version increments", async () => {
|
||||
const rectA = API.createElement({
|
||||
type: "line",
|
||||
id: "A",
|
||||
subtype: test1.subtype,
|
||||
roughness: 1,
|
||||
strokeWidth: 1,
|
||||
});
|
||||
const rectB = API.createElement({
|
||||
type: "line",
|
||||
id: "B",
|
||||
subtype: test1.subtype,
|
||||
roughness: 1,
|
||||
strokeWidth: 1,
|
||||
});
|
||||
// Initial element creation checks
|
||||
expect(rectA.roughness).toBe(0);
|
||||
expect(rectB.roughness).toBe(0);
|
||||
expect(rectA.version).toBe(1);
|
||||
expect(rectB.version).toBe(1);
|
||||
// Check that attempting to set prop values not permitted by the subtype
|
||||
// doesn't increment element versions
|
||||
mutateElement(rectA, { roughness: 2 });
|
||||
mutateElement(rectB, { roughness: 2, strokeWidth: 2 });
|
||||
expect(rectA.version).toBe(1);
|
||||
expect(rectB.version).toBe(2);
|
||||
// Check that element versions don't increment when creating new elements
|
||||
// while attempting to use prop values not permitted by the subtype
|
||||
// First check based on `rectA` (unsuccessfully mutated)
|
||||
const rectC = newElementWith(rectA, { roughness: 1 });
|
||||
const rectD = newElementWith(rectA, { roughness: 1, strokeWidth: 1.5 });
|
||||
expect(rectC.version).toBe(1);
|
||||
expect(rectD.version).toBe(2);
|
||||
// Then check based on `rectB` (successfully mutated)
|
||||
const rectE = newElementWith(rectB, { roughness: 1 });
|
||||
const rectF = newElementWith(rectB, { roughness: 1, strokeWidth: 1.5 });
|
||||
expect(rectE.version).toBe(2);
|
||||
expect(rectF.version).toBe(3);
|
||||
});
|
||||
it("should call custom text methods", async () => {
|
||||
const testString = "A quick brown fox jumps over the lazy dog.";
|
||||
const elements = [
|
||||
API.createElement({
|
||||
type: "text",
|
||||
id: "A",
|
||||
subtype: test2.subtype,
|
||||
text: testString,
|
||||
fontSize: FONTSIZE,
|
||||
}),
|
||||
];
|
||||
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||
const mockMeasureText = (text: string, font: FontString) => {
|
||||
if (text === testString) {
|
||||
let multiplier = 1;
|
||||
if (font.includes(`${DBFONTSIZE}`)) {
|
||||
multiplier = 2;
|
||||
}
|
||||
if (font.includes(`${TRFONTSIZE}`)) {
|
||||
multiplier = 3;
|
||||
}
|
||||
const width = multiplier * TWIDTH;
|
||||
const height = multiplier * THEIGHT;
|
||||
const baseline = multiplier * TBASELINE;
|
||||
return { width, height, baseline };
|
||||
}
|
||||
return { width: 1, height: 0, baseline: 0 };
|
||||
};
|
||||
|
||||
vi.spyOn(textElementUtils, "measureText").mockImplementation(
|
||||
mockMeasureText,
|
||||
);
|
||||
|
||||
elements.forEach((el) => {
|
||||
if (isTextElement(el)) {
|
||||
// First test with `ExcalidrawTextElement.text`
|
||||
const metrics = textElementUtils.measureTextElement(el);
|
||||
expect(metrics).toStrictEqual({
|
||||
width: TWIDTH - 10,
|
||||
height: THEIGHT - 5,
|
||||
baseline: TBASELINE + 1,
|
||||
});
|
||||
const wrappedText = textElementUtils.wrapTextElement(el, MW);
|
||||
expect(wrappedText).toEqual(
|
||||
`${testString.split(" ").join("\n")}\nHello world.`,
|
||||
);
|
||||
|
||||
// Now test with modified text in `next`
|
||||
let next: {
|
||||
text?: string;
|
||||
fontSize?: number;
|
||||
customData?: Record<string, any>;
|
||||
} = {
|
||||
text: "Hello world.",
|
||||
};
|
||||
const nextMetrics = textElementUtils.measureTextElement(el, next);
|
||||
expect(nextMetrics).toStrictEqual({ width: 0, height: 0, baseline: 1 });
|
||||
const nextWrappedText = textElementUtils.wrapTextElement(el, MW, next);
|
||||
expect(nextWrappedText).toEqual("Hello\nworld.\nHello world.");
|
||||
|
||||
// Now test modified fontSizes in `next`
|
||||
next = { fontSize: DBFONTSIZE };
|
||||
const nextFM = textElementUtils.measureTextElement(el, next);
|
||||
expect(nextFM).toStrictEqual({
|
||||
width: 2 * TWIDTH - 10,
|
||||
height: 2 * THEIGHT - 5,
|
||||
baseline: 2 * TBASELINE + 1,
|
||||
});
|
||||
const nextFWrText = textElementUtils.wrapTextElement(el, MW, next);
|
||||
expect(nextFWrText).toEqual(
|
||||
`${testString.split(" ").join("\n")}\nHELLO World.`,
|
||||
);
|
||||
|
||||
// Now test customData in `next`
|
||||
next = { customData: { triple: true } };
|
||||
const nextCD = textElementUtils.measureTextElement(el, next);
|
||||
expect(nextCD).toStrictEqual({
|
||||
width: 3 * TWIDTH - 10,
|
||||
height: 3 * THEIGHT - 5,
|
||||
baseline: 3 * TBASELINE + 1,
|
||||
});
|
||||
const nextCDWrText = textElementUtils.wrapTextElement(el, MW, next);
|
||||
expect(nextCDWrText).toEqual(
|
||||
`${testString.split(" ").join("\n")}\nHELLO WORLD.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
it("should recognize subtypes with always-enabled actions", async () => {
|
||||
expect(hasAlwaysEnabledActions(test1.subtype)).toBe(false);
|
||||
expect(hasAlwaysEnabledActions(test2.subtype)).toBe(false);
|
||||
expect(hasAlwaysEnabledActions(test3.subtype)).toBe(true);
|
||||
});
|
||||
it("should select active subtypes and customData", async () => {
|
||||
const appState = {} as {
|
||||
activeSubtypes: AppState["activeSubtypes"];
|
||||
customData: AppState["customData"];
|
||||
};
|
||||
|
||||
// No active subtypes
|
||||
let subtypes = selectSubtype(appState, "text");
|
||||
expect(subtypes.subtype).toBeUndefined();
|
||||
expect(subtypes.customData).toBeUndefined();
|
||||
// Subtype for both "text" and "line" types
|
||||
appState.activeSubtypes = [test3.subtype];
|
||||
subtypes = selectSubtype(appState, "text");
|
||||
expect(subtypes.subtype).toBe(test3.subtype);
|
||||
subtypes = selectSubtype(appState, "line");
|
||||
expect(subtypes.subtype).toBe(test3.subtype);
|
||||
subtypes = selectSubtype(appState, "arrow");
|
||||
expect(subtypes.subtype).toBeUndefined();
|
||||
// Subtype for multiple linear types
|
||||
appState.activeSubtypes = [test1.subtype];
|
||||
subtypes = selectSubtype(appState, "text");
|
||||
expect(subtypes.subtype).toBeUndefined();
|
||||
subtypes = selectSubtype(appState, "line");
|
||||
expect(subtypes.subtype).toBe(test1.subtype);
|
||||
subtypes = selectSubtype(appState, "arrow");
|
||||
expect(subtypes.subtype).toBe(test1.subtype);
|
||||
// Subtype for "text" only
|
||||
appState.activeSubtypes = [test2.subtype];
|
||||
subtypes = selectSubtype(appState, "text");
|
||||
expect(subtypes.subtype).toBe(test2.subtype);
|
||||
subtypes = selectSubtype(appState, "line");
|
||||
expect(subtypes.subtype).toBeUndefined();
|
||||
subtypes = selectSubtype(appState, "arrow");
|
||||
expect(subtypes.subtype).toBeUndefined();
|
||||
|
||||
// Test customData
|
||||
appState.customData = {};
|
||||
appState.customData[test1.subtype] = { test: true };
|
||||
appState.customData[test2.subtype] = { test2: true };
|
||||
appState.customData[test3.subtype] = { test3: true };
|
||||
// Subtype for both "text" and "line" types
|
||||
appState.activeSubtypes = [test3.subtype];
|
||||
subtypes = selectSubtype(appState, "text");
|
||||
expect(subtypes.customData).toBeDefined();
|
||||
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
||||
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
||||
expect(subtypes.customData![test3.subtype]).toBe(true);
|
||||
subtypes = selectSubtype(appState, "line");
|
||||
expect(subtypes.customData).toBeDefined();
|
||||
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
||||
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
||||
expect(subtypes.customData![test3.subtype]).toBe(true);
|
||||
subtypes = selectSubtype(appState, "arrow");
|
||||
expect(subtypes.customData).toBeUndefined();
|
||||
// Subtype for multiple linear types
|
||||
appState.activeSubtypes = [test1.subtype];
|
||||
subtypes = selectSubtype(appState, "text");
|
||||
expect(subtypes.customData).toBeUndefined();
|
||||
subtypes = selectSubtype(appState, "line");
|
||||
expect(subtypes.customData).toBeDefined();
|
||||
expect(subtypes.customData![test1.subtype]).toBe(true);
|
||||
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
||||
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
||||
// Multiple, non-colliding subtypes
|
||||
appState.activeSubtypes = [test1.subtype, test2.subtype];
|
||||
subtypes = selectSubtype(appState, "text");
|
||||
expect(subtypes.customData).toBeDefined();
|
||||
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
||||
expect(subtypes.customData![test2.subtype]).toBe(true);
|
||||
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
||||
subtypes = selectSubtype(appState, "line");
|
||||
expect(subtypes.customData).toBeDefined();
|
||||
expect(subtypes.customData![test1.subtype]).toBe(true);
|
||||
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
||||
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe("subtype actions", () => {
|
||||
let elements: ExcalidrawElement[];
|
||||
beforeEach(async () => {
|
||||
elements = [
|
||||
API.createElement({ type: "line", id: "A", subtype: test1.subtype }),
|
||||
API.createElement({ type: "line", id: "B" }),
|
||||
API.createElement({ type: "line", id: "C", subtype: test3.subtype }),
|
||||
API.createElement({ type: "text", id: "D", subtype: test3.subtype }),
|
||||
];
|
||||
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||
});
|
||||
it("should apply to elements with their subtype", async () => {
|
||||
h.setState({ selectedElementIds: { A: true } });
|
||||
const am = h.app.actionManager;
|
||||
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
||||
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(false);
|
||||
});
|
||||
it("should apply to elements without a subtype", async () => {
|
||||
h.setState({ selectedElementIds: { B: true } });
|
||||
const am = h.app.actionManager;
|
||||
expect(am.isActionEnabled(testAction, { elements })).toBe(false);
|
||||
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||
});
|
||||
it("should apply to elements with and without their subtype", async () => {
|
||||
h.setState({ selectedElementIds: { A: true, B: true } });
|
||||
const am = h.app.actionManager;
|
||||
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
||||
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||
});
|
||||
it("should apply to elements with a different subtype", async () => {
|
||||
h.setState({ selectedElementIds: { C: true, D: true } });
|
||||
const am = h.app.actionManager;
|
||||
expect(am.isActionEnabled(testAction, { elements })).toBe(false);
|
||||
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||
});
|
||||
it("should apply to like types with varying subtypes", async () => {
|
||||
h.setState({ selectedElementIds: { A: true, C: true } });
|
||||
const am = h.app.actionManager;
|
||||
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
||||
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||
});
|
||||
it("should apply to non-like types with varying subtypes", async () => {
|
||||
h.setState({ selectedElementIds: { A: true, D: true } });
|
||||
const am = h.app.actionManager;
|
||||
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
||||
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(false);
|
||||
});
|
||||
it("should apply to like/non-like types with varying subtypes", async () => {
|
||||
h.setState({ selectedElementIds: { A: true, B: true, D: true } });
|
||||
const am = h.app.actionManager;
|
||||
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
||||
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
||||
});
|
||||
it("should apply to the correct parent type", async () => {
|
||||
const am = h.app.actionManager;
|
||||
h.setState({ selectedElementIds: { A: true, C: true } });
|
||||
expect(am.isActionEnabled(TEST_DISABLE3, { elements })).toBe(true);
|
||||
h.setState({ selectedElementIds: { A: true, D: true } });
|
||||
expect(am.isActionEnabled(TEST_DISABLE3, { elements })).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("subtype loading", () => {
|
||||
let elements: ExcalidrawElement[];
|
||||
beforeEach(async () => {
|
||||
const testString = "A quick brown fox jumps over the lazy dog.";
|
||||
elements = [
|
||||
API.createElement({
|
||||
type: "text",
|
||||
id: "A",
|
||||
subtype: test2.subtype,
|
||||
text: testString,
|
||||
}),
|
||||
];
|
||||
await render(<Excalidraw />, { localStorageData: { elements } });
|
||||
h.elements = elements;
|
||||
});
|
||||
it("should redraw text bounding boxes", async () => {
|
||||
h.setState({ selectedElementIds: { A: true } });
|
||||
const el = h.elements[0] as ExcalidrawTextElement;
|
||||
expect(el.width).toEqual(100);
|
||||
expect(el.height).toEqual(100);
|
||||
ensureSubtypesLoadedForElements(elements);
|
||||
expect(el.width).toEqual(TWIDTH * 2);
|
||||
expect(el.height).toEqual(THEIGHT * 2);
|
||||
expect(el.baseline).toEqual(TBASELINE + 1);
|
||||
});
|
||||
});
|
16
src/types.ts
16
src/types.ts
@ -18,6 +18,7 @@ import {
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
} from "./element/types";
|
||||
import { Action } from "./actions/types";
|
||||
import { SHAPES } from "./shapes";
|
||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import { LinearElementEditor } from "./element/linearElementEditor";
|
||||
@ -31,6 +32,12 @@ import { ClipboardData } from "./clipboard";
|
||||
import { isOverScrollBars } from "./scene";
|
||||
import { MaybeTransformHandleType } from "./element/transformHandles";
|
||||
import Library from "./data/library";
|
||||
import {
|
||||
SubtypeMethods,
|
||||
Subtype,
|
||||
SubtypePrepFn,
|
||||
SubtypeRecord,
|
||||
} from "./element/subtypes";
|
||||
import type { FileSystemHandle } from "./data/filesystem";
|
||||
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||
import { ContextMenuItems } from "./components/ContextMenu";
|
||||
@ -186,6 +193,10 @@ export type AppState = {
|
||||
// (e.g. text element when typing into the input)
|
||||
editingElement: NonDeletedExcalidrawElement | null;
|
||||
editingLinearElement: LinearElementEditor | null;
|
||||
activeSubtypes?: Subtype[];
|
||||
customData?: {
|
||||
[subtype: Subtype]: ExcalidrawElement["customData"];
|
||||
};
|
||||
activeTool: {
|
||||
/**
|
||||
* indicates a previous tool we should revert back to if we deselect the
|
||||
@ -609,6 +620,11 @@ export type ExcalidrawImperativeAPI = {
|
||||
getSceneElements: InstanceType<typeof App>["getSceneElements"];
|
||||
getAppState: () => InstanceType<typeof App>["state"];
|
||||
getFiles: () => InstanceType<typeof App>["files"];
|
||||
actionManager: InstanceType<typeof App>["actionManager"];
|
||||
addSubtype: (
|
||||
record: SubtypeRecord,
|
||||
subtypePrepFn: SubtypePrepFn,
|
||||
) => { actions: Action[] | null; methods: Partial<SubtypeMethods> };
|
||||
refresh: InstanceType<typeof App>["refresh"];
|
||||
setToast: InstanceType<typeof App>["setToast"];
|
||||
addFiles: (data: BinaryFileData[]) => void;
|
||||
|
323
yarn.lock
323
yarn.lock
@ -2878,6 +2878,16 @@
|
||||
loupe "^2.3.6"
|
||||
pretty-format "^29.5.0"
|
||||
|
||||
"@xmldom/xmldom@0.9.0-beta.8":
|
||||
version "0.9.0-beta.8"
|
||||
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.9.0-beta.8.tgz#ef343e627e4d5eba57c52b40c2e1f26d12738512"
|
||||
integrity sha512-Q5bFbYxRJKTYP7S1a0HIlumTmJRHHMGrNvBp8F1mUEyyGTeCs0g8+FKAaA6tU+YFsZgHKA0eRKzZhYdhpgAHAw==
|
||||
|
||||
"@yarnpkg/lockfile@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
||||
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
|
||||
|
||||
abab@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
||||
@ -3395,7 +3405,7 @@ check-error@^1.0.2:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
ci-info@^3.2.0:
|
||||
ci-info@^3.2.0, ci-info@^3.7.0:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
|
||||
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
|
||||
@ -3437,6 +3447,15 @@ cliui@^7.0.2:
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clsx@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
@ -3478,6 +3497,11 @@ combined-stream@^1.0.8:
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1"
|
||||
integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==
|
||||
|
||||
commander@^2.20.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
@ -3523,6 +3547,19 @@ convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
|
||||
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
|
||||
|
||||
copyfiles@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5"
|
||||
integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==
|
||||
dependencies:
|
||||
glob "^7.0.5"
|
||||
minimatch "^3.0.3"
|
||||
mkdirp "^1.0.4"
|
||||
noms "0.0.0"
|
||||
through2 "^2.0.1"
|
||||
untildify "^4.0.0"
|
||||
yargs "^16.1.0"
|
||||
|
||||
core-js-compat@^3.25.1:
|
||||
version "3.30.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.0.tgz#99aa2789f6ed2debfa1df3232784126ee97f4d80"
|
||||
@ -3540,6 +3577,11 @@ core-js@^3.4:
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.0.tgz#64ac6f83bc7a49fd42807327051701d4b1478dea"
|
||||
integrity sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||
|
||||
corser@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
|
||||
@ -4401,6 +4443,13 @@ fill-range@^7.0.1:
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
find-yarn-workspace-root@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
|
||||
integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
|
||||
dependencies:
|
||||
micromatch "^4.0.2"
|
||||
|
||||
firebase@8.3.3:
|
||||
version "8.3.3"
|
||||
resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.3.3.tgz#21d8fb8eec2c43b0d8f98ab6bda5535b7454fa54"
|
||||
@ -4464,7 +4513,7 @@ fs-extra@^11.1.0:
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-extra@^9.0.1:
|
||||
fs-extra@^9.0.0, fs-extra@^9.0.1:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
|
||||
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
|
||||
@ -4563,7 +4612,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
@ -4575,6 +4624,17 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
|
||||
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^5.0.1"
|
||||
once "^1.3.0"
|
||||
|
||||
globals@^11.1.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
@ -4618,7 +4678,7 @@ gopd@^1.0.1:
|
||||
dependencies:
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9:
|
||||
graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
@ -4852,7 +4912,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@ -4943,6 +5003,11 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
|
||||
dependencies:
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-docker@^2.0.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
||||
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
@ -5077,6 +5142,18 @@ is-weakset@^2.0.1:
|
||||
call-bind "^1.0.2"
|
||||
get-intrinsic "^1.1.1"
|
||||
|
||||
is-wsl@^2.1.1:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
||||
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
|
||||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
|
||||
|
||||
isarray@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
|
||||
@ -5087,6 +5164,11 @@ isarray@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
@ -5299,6 +5381,13 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
json-stable-stringify@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0"
|
||||
integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==
|
||||
dependencies:
|
||||
jsonify "^0.0.1"
|
||||
|
||||
json5@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||
@ -5325,6 +5414,11 @@ jsonfile@^6.0.1:
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonify@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978"
|
||||
integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==
|
||||
|
||||
jsonpointer@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
|
||||
@ -5338,6 +5432,13 @@ jsonpointer@^5.0.0:
|
||||
array-includes "^3.1.5"
|
||||
object.assign "^4.1.3"
|
||||
|
||||
klaw-sync@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
|
||||
integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.1.11"
|
||||
|
||||
language-subtag-registry@~0.3.2:
|
||||
version "0.3.22"
|
||||
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
|
||||
@ -5547,6 +5648,20 @@ make-dir@^4.0.0:
|
||||
dependencies:
|
||||
semver "^7.5.3"
|
||||
|
||||
"mathjax-full@https://github.com/MathJax/MathJax-src#develop":
|
||||
version "4.0.0-beta.3"
|
||||
resolved "https://github.com/MathJax/MathJax-src#9ca76266fc7b26ddab4efb983eebe900e3a428c0"
|
||||
dependencies:
|
||||
mathjax-modern-font "^4.0.0-beta.3"
|
||||
mhchemparser "^4.2.1"
|
||||
mj-context-menu "^0.9.1"
|
||||
speech-rule-engine "^4.1.0-beta.7"
|
||||
|
||||
mathjax-modern-font@^4.0.0-beta.3:
|
||||
version "4.0.0-beta.3"
|
||||
resolved "https://registry.yarnpkg.com/mathjax-modern-font/-/mathjax-modern-font-4.0.0-beta.3.tgz#2d032255f47e7730e3beb5d3061606f5f1e8c376"
|
||||
integrity sha512-rUD7Hxu2yKCogWg0PVx5l4iMn2mOjcD4/vseIU+u2RxFkRfEDvfCphdBiQv27O8wnYEbdhjmn9+v4cDeDowDWg==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
@ -5557,7 +5672,12 @@ merge2@^1.3.0, merge2@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
micromatch@^4.0.4:
|
||||
mhchemparser@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/mhchemparser/-/mhchemparser-4.2.1.tgz#d73982e66bc06170a85b1985600ee9dabe157cb0"
|
||||
integrity sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==
|
||||
|
||||
micromatch@^4.0.2, micromatch@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||
@ -5592,7 +5712,7 @@ min-indent@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
||||
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
|
||||
|
||||
minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
@ -5611,6 +5731,11 @@ minimist@^1.2.0, minimist@^1.2.6:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
mj-context-menu@^0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/mj-context-menu/-/mj-context-menu-0.9.1.tgz#8195fb10092488d9feeba8b50f03fa56080edb14"
|
||||
integrity sha512-ECPcVXZFRfeYOxb1MWGzctAtnQcZ6nRucE3orfkKX7t/KE2mlXO2K/bq4BcCGOuhdz3Wg2BZDy2S8ECK73/iIw==
|
||||
|
||||
mkdirp@^0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
@ -5618,6 +5743,11 @@ mkdirp@^0.5.6:
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
mlly@^1.2.0, mlly@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.0.tgz#830c10d63f1f97bd8785377b24dc2a15d972832b"
|
||||
@ -5693,6 +5823,14 @@ node-releases@^2.0.8:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
|
||||
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
|
||||
|
||||
noms@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859"
|
||||
integrity sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==
|
||||
dependencies:
|
||||
inherits "^2.0.1"
|
||||
readable-stream "~1.0.31"
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
@ -5797,6 +5935,14 @@ open-color@1.9.1:
|
||||
resolved "https://registry.yarnpkg.com/open-color/-/open-color-1.9.1.tgz#a6e6328f60eff7aa60e3e8fcfa50f53ff3eece35"
|
||||
integrity sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==
|
||||
|
||||
open@^7.4.2:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
|
||||
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
|
||||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
opener@^1.5.1:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||
@ -5814,6 +5960,11 @@ optionator@^0.9.1:
|
||||
type-check "^0.4.0"
|
||||
word-wrap "^1.2.3"
|
||||
|
||||
os-tmpdir@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
|
||||
|
||||
p-limit@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644"
|
||||
@ -5867,6 +6018,27 @@ parseuri@0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
|
||||
integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
|
||||
|
||||
patch-package@8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61"
|
||||
integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==
|
||||
dependencies:
|
||||
"@yarnpkg/lockfile" "^1.1.0"
|
||||
chalk "^4.1.2"
|
||||
ci-info "^3.7.0"
|
||||
cross-spawn "^7.0.3"
|
||||
find-yarn-workspace-root "^2.0.0"
|
||||
fs-extra "^9.0.0"
|
||||
json-stable-stringify "^1.0.2"
|
||||
klaw-sync "^6.0.0"
|
||||
minimist "^1.2.6"
|
||||
open "^7.4.2"
|
||||
rimraf "^2.6.3"
|
||||
semver "^7.5.3"
|
||||
slash "^2.0.0"
|
||||
tmp "^0.0.33"
|
||||
yaml "^2.2.2"
|
||||
|
||||
path-data-parser@0.1.0, path-data-parser@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/path-data-parser/-/path-data-parser-0.1.0.tgz#8f5ba5cc70fc7becb3dcefaea08e2659aba60b8c"
|
||||
@ -6007,6 +6179,11 @@ postcss@^8.4.24:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postinstall-postinstall@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3"
|
||||
integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
@ -6052,6 +6229,11 @@ pretty-format@^29.0.0, pretty-format@^29.5.0:
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^18.0.0"
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||
|
||||
progress@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
@ -6210,6 +6392,29 @@ react@18.2.0:
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
readable-stream@~1.0.31:
|
||||
version "1.0.34"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
|
||||
integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@~2.3.6:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
|
||||
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||
@ -6292,6 +6497,15 @@ regjsparser@^0.9.1:
|
||||
dependencies:
|
||||
jsesc "~0.5.0"
|
||||
|
||||
replace-in-file@7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/replace-in-file/-/replace-in-file-7.0.1.tgz#1bb69a2e5596341cc6f0f581309add6c1d364b71"
|
||||
integrity sha512-KbhgPq04eA+TxXuUxpgWIH9k/TjF+28ofon2PXP7vq6izAILhxOtksCVcLuuQLtyjouBaPdlH6RJYYcSPVxCOA==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
glob "^8.1.0"
|
||||
yargs "^17.7.2"
|
||||
|
||||
require-directory@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
@ -6355,6 +6569,13 @@ rfdc@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
|
||||
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
|
||||
|
||||
rimraf@^2.6.3:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
@ -6426,7 +6647,7 @@ safari-14-idb-fix@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440"
|
||||
integrity sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
@ -6544,6 +6765,11 @@ sirv@^2.0.3:
|
||||
mrmime "^1.0.0"
|
||||
totalist "^3.0.0"
|
||||
|
||||
slash@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
|
||||
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
|
||||
|
||||
slash@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
@ -6644,6 +6870,15 @@ sourcemap-codec@^1.4.8:
|
||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||
|
||||
speech-rule-engine@^4.1.0-beta.7:
|
||||
version "4.1.0-beta.7"
|
||||
resolved "https://registry.yarnpkg.com/speech-rule-engine/-/speech-rule-engine-4.1.0-beta.7.tgz#b3690bbe27582c07c86631e11cc045e3d3b154cc"
|
||||
integrity sha512-e9QntjrfSKDa/w0baCXsoPQRPD9uY0r7q86Jr8ud/5zElzdG0Beiz4mc38kFb/E53c4RuYyZKSKyug8e5cVrpQ==
|
||||
dependencies:
|
||||
"@xmldom/xmldom" "0.9.0-beta.8"
|
||||
commander "10.0.0"
|
||||
wicked-good-xpath "1.3.0"
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
@ -6742,6 +6977,18 @@ string.prototype.trimstart@^1.0.6:
|
||||
define-properties "^1.1.4"
|
||||
es-abstract "^1.20.4"
|
||||
|
||||
string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
stringify-object@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629"
|
||||
@ -6883,6 +7130,14 @@ text-table@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
|
||||
|
||||
through2@^2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
|
||||
dependencies:
|
||||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
through@^2.3.8:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
@ -6908,6 +7163,13 @@ tinyspy@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.1.tgz#9e6371b00c259e5c5b301917ca18c01d40ae558c"
|
||||
integrity sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
|
||||
dependencies:
|
||||
os-tmpdir "~1.0.2"
|
||||
|
||||
to-array@0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
|
||||
@ -7112,6 +7374,11 @@ universalify@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
untildify@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
|
||||
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
|
||||
|
||||
upath@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
|
||||
@ -7170,6 +7437,11 @@ use-sync-external-store@1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
@ -7480,6 +7752,11 @@ why-is-node-running@^2.2.2:
|
||||
siginfo "^2.0.0"
|
||||
stackback "0.0.2"
|
||||
|
||||
wicked-good-xpath@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz#81b0e95e8650e49c94b22298fff8686b5553cf6c"
|
||||
integrity sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==
|
||||
|
||||
word-wrap@^1.2.3:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
@ -7698,6 +7975,11 @@ xmlhttprequest@1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
|
||||
integrity sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==
|
||||
|
||||
xtend@~4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
@ -7718,12 +8000,22 @@ yaml@^1.10.0, yaml@^1.10.2:
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
||||
yaml@^2.2.2:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
||||
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
|
||||
|
||||
yargs-parser@^20.2.2:
|
||||
version "20.2.9"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||
|
||||
yargs@^16.2.0:
|
||||
yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^16.1.0, yargs@^16.2.0:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
||||
@ -7736,6 +8028,19 @@ yargs@^16.2.0:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yargs@^17.7.2:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
||||
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
||||
dependencies:
|
||||
cliui "^8.0.1"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
yeast@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
|
Loading…
x
Reference in New Issue
Block a user