Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8ae1d2486 | ||
|
|
482178b7b6 | ||
|
|
7da500fe0a | ||
|
|
b1d923db3a | ||
|
|
7f6789780d | ||
|
|
17accffee3 | ||
|
|
e1b0b559f5 | ||
|
|
8654958aba | ||
|
|
7c1889efc0 | ||
|
|
b3e8d6bbf0 | ||
|
|
26481fbf43 | ||
|
|
b7a4dfb6ef | ||
|
|
8a153b822a | ||
|
|
57da3768b1 | ||
|
|
db163ab89c | ||
|
|
3593cf6a14 | ||
|
|
ce6a9986e3 | ||
|
|
c95369ace8 | ||
|
|
a8283b27c5 | ||
|
|
3ea7c7ffa0 | ||
|
|
beb2ceaa72 | ||
|
|
0d49eb3e05 | ||
|
|
de0f35454a | ||
|
|
d393a1c048 | ||
|
|
23fa44cf5e | ||
|
|
9d22e84fc8 | ||
|
|
aa6b585b96 | ||
|
|
06389f96b9 | ||
|
|
9b1c88eed9 | ||
|
|
d070760b2f | ||
|
|
acd6db5304 | ||
|
|
d203794f70 | ||
|
|
c8fd157914 | ||
|
|
b47d46699a | ||
|
|
10a5d18fa2 | ||
|
|
b1bd94c377 | ||
|
|
e4e2e044af | ||
|
|
d3db48e8f4 | ||
|
|
cef0f15158 | ||
|
|
742e4bc017 | ||
|
|
a573fbbb5a | ||
|
|
f696d36e03 | ||
|
|
21a804a4e5 | ||
|
|
c313a2cf42 | ||
|
|
6b33ba1d04 | ||
|
|
fe9f89a0fc | ||
|
|
d1266280cb | ||
|
|
41a0258fe1 | ||
|
|
3346c72356 | ||
|
|
6518e05cab | ||
|
|
f85f890b25 | ||
|
|
7b612bec5e | ||
|
|
efdafc8659 | ||
|
|
554913c58b | ||
|
|
84059086a6 | ||
|
|
fec984895e | ||
|
|
c5514974b7 | ||
|
|
6a04ebc6db |
@@ -131,5 +131,5 @@ export class Debug {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
//@ts-ignore
|
||||||
window.debug = Debug;
|
window.debug = Debug;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { trackEvent } from "../src/analytics";
|
|||||||
import { getDefaultAppState } from "../src/appState";
|
import { getDefaultAppState } from "../src/appState";
|
||||||
import { ErrorDialog } from "../src/components/ErrorDialog";
|
import { ErrorDialog } from "../src/components/ErrorDialog";
|
||||||
import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
|
import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
|
||||||
import { useMathSubtype } from "../src/element/subtypes/mathjax";
|
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
EVENT,
|
EVENT,
|
||||||
@@ -304,8 +303,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
const [excalidrawAPI, excalidrawRefCallback] =
|
const [excalidrawAPI, excalidrawRefCallback] =
|
||||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||||
|
|
||||||
useMathSubtype(excalidrawAPI);
|
|
||||||
|
|
||||||
const [collabAPI] = useAtom(collabAPIAtom);
|
const [collabAPI] = useAtom(collabAPIAtom);
|
||||||
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
||||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||||
|
|||||||
+2
-7
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
|
"@excalidraw/mermaid-to-excalidraw": "0.1.1",
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
@@ -31,7 +32,6 @@
|
|||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"copyfiles": "2.4.1",
|
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
@@ -41,22 +41,18 @@
|
|||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
"jotai": "1.13.1",
|
"jotai": "1.13.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"mathjax-full": "https://github.com/MathJax/MathJax-src#develop",
|
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"open-color": "1.9.1",
|
"open-color": "1.9.1",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
"patch-package": "8.0.0",
|
|
||||||
"perfect-freehand": "1.2.0",
|
"perfect-freehand": "1.2.0",
|
||||||
"pica": "7.1.1",
|
"pica": "7.1.1",
|
||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
"png-chunks-encode": "1.0.0",
|
"png-chunks-encode": "1.0.0",
|
||||||
"png-chunks-extract": "1.0.0",
|
"png-chunks-extract": "1.0.0",
|
||||||
"points-on-curve": "0.2.0",
|
"points-on-curve": "0.2.0",
|
||||||
"postinstall-postinstall": "2.1.0",
|
|
||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"replace-in-file": "7.0.1",
|
|
||||||
"roughjs": "4.5.2",
|
"roughjs": "4.5.2",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
@@ -116,7 +112,6 @@
|
|||||||
"fix": "yarn fix:other && yarn fix:code",
|
"fix": "yarn fix:other && yarn fix:code",
|
||||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||||
"locales-coverage:description": "node scripts/locales-coverage-description.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",
|
"prepare": "husky install",
|
||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
@@ -130,7 +125,7 @@
|
|||||||
"test": "yarn test:app",
|
"test": "yarn test:app",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"test:coverage:watch": "vitest --coverage --watch",
|
"test:coverage:watch": "vitest --coverage --watch",
|
||||||
"test:ui": "yarn test --ui",
|
"test:ui": "yarn test --ui --coverage.enabled=true",
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease": "node scripts/prerelease.js",
|
"prerelease": "node scripts/prerelease.js",
|
||||||
"build:preview": "yarn build && vite preview --port 5000",
|
"build:preview": "yarn build && vite preview --port 5000",
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
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++) {
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// 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,
|
computeBoundTextPosition,
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureTextElement,
|
measureText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
|
import { getFontString } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionUnbindText = register({
|
export const actionUnbindText = register({
|
||||||
@@ -47,11 +48,10 @@ export const actionUnbindText = register({
|
|||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const { width, height, baseline } = measureTextElement(
|
const { width, height, baseline } = measureText(
|
||||||
boundTextElement,
|
boundTextElement.originalText,
|
||||||
{
|
getFontString(boundTextElement),
|
||||||
text: boundTextElement.originalText,
|
boundTextElement.lineHeight,
|
||||||
},
|
|
||||||
);
|
);
|
||||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||||
element.id,
|
element.id,
|
||||||
|
|||||||
+11
-62
@@ -2,10 +2,10 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
UpdaterFn,
|
UpdaterFn,
|
||||||
|
ActionName,
|
||||||
ActionResult,
|
ActionResult,
|
||||||
PanelComponentProps,
|
PanelComponentProps,
|
||||||
ActionSource,
|
ActionSource,
|
||||||
ActionPredicateFn,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
@@ -40,8 +40,7 @@ const trackAction = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ActionManager {
|
export class ActionManager {
|
||||||
actions = {} as Record<Action["name"], Action>;
|
actions = {} as Record<ActionName, Action>;
|
||||||
actionPredicates = [] as ActionPredicateFn[];
|
|
||||||
|
|
||||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||||
|
|
||||||
@@ -69,37 +68,6 @@ export class ActionManager {
|
|||||||
this.app = app;
|
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) {
|
registerAction(action: Action) {
|
||||||
this.actions[action.name] = action;
|
this.actions[action.name] = action;
|
||||||
}
|
}
|
||||||
@@ -116,7 +84,7 @@ export class ActionManager {
|
|||||||
(action) =>
|
(action) =>
|
||||||
(action.name in canvasActions
|
(action.name in canvasActions
|
||||||
? canvasActions[action.name as keyof typeof canvasActions]
|
? canvasActions[action.name as keyof typeof canvasActions]
|
||||||
: this.isActionEnabled(action, { noPredicates: true })) &&
|
: true) &&
|
||||||
action.keyTest &&
|
action.keyTest &&
|
||||||
action.keyTest(
|
action.keyTest(
|
||||||
event,
|
event,
|
||||||
@@ -167,7 +135,7 @@ export class ActionManager {
|
|||||||
/**
|
/**
|
||||||
* @param data additional data sent to the PanelComponent
|
* @param data additional data sent to the PanelComponent
|
||||||
*/
|
*/
|
||||||
renderAction = (name: Action["name"], data?: PanelComponentProps["data"]) => {
|
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -175,7 +143,7 @@ export class ActionManager {
|
|||||||
"PanelComponent" in this.actions[name] &&
|
"PanelComponent" in this.actions[name] &&
|
||||||
(name in canvasActions
|
(name in canvasActions
|
||||||
? canvasActions[name as keyof typeof canvasActions]
|
? canvasActions[name as keyof typeof canvasActions]
|
||||||
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
|
: true)
|
||||||
) {
|
) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
@@ -197,7 +165,6 @@ export class ActionManager {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
key={name}
|
|
||||||
elements={this.getElementsIncludingDeleted()}
|
elements={this.getElementsIncludingDeleted()}
|
||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
@@ -211,31 +178,13 @@ export class ActionManager {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
isActionEnabled = (
|
isActionEnabled = (action: Action) => {
|
||||||
action: Action,
|
const elements = this.getElementsIncludingDeleted();
|
||||||
opts?: {
|
|
||||||
elements?: readonly ExcalidrawElement[];
|
|
||||||
data?: Record<string, any>;
|
|
||||||
noPredicates?: boolean;
|
|
||||||
},
|
|
||||||
): boolean => {
|
|
||||||
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
const data = opts?.data;
|
|
||||||
|
|
||||||
if (
|
return (
|
||||||
!opts?.noPredicates &&
|
!action.predicate ||
|
||||||
action.predicate &&
|
action.predicate(elements, appState, this.app.props, this.app)
|
||||||
!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,23 +83,8 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomShortcutName = string;
|
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||||
|
const shortcuts = shortcutMap[name];
|
||||||
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
|
// if multiple shortcuts available, take the first one
|
||||||
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-11
@@ -32,15 +32,6 @@ type ActionFn = (
|
|||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => 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 UpdaterFn = (res: ActionResult) => void;
|
||||||
export type ActionFilterFn = (action: Action) => void;
|
export type ActionFilterFn = (action: Action) => void;
|
||||||
|
|
||||||
@@ -144,7 +135,7 @@ export type PanelComponentProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: string;
|
name: ActionName;
|
||||||
PanelComponent?: React.FC<PanelComponentProps>;
|
PanelComponent?: React.FC<PanelComponentProps>;
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
@@ -166,7 +157,6 @@ export interface Action {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
appProps: ExcalidrawProps,
|
appProps: ExcalidrawProps,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
data?: Record<string, any>,
|
|
||||||
) => boolean;
|
) => boolean;
|
||||||
checked?: (appState: Readonly<AppState>) => boolean;
|
checked?: (appState: Readonly<AppState>) => boolean;
|
||||||
trackEvent:
|
trackEvent:
|
||||||
|
|||||||
@@ -146,8 +146,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
editingGroupId: { browser: true, export: false, server: false },
|
editingGroupId: { browser: true, export: false, server: false },
|
||||||
editingLinearElement: { browser: false, export: false, server: false },
|
editingLinearElement: { browser: false, export: false, server: false },
|
||||||
activeTool: { browser: true, 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 },
|
penMode: { browser: true, export: false, server: false },
|
||||||
penDetected: { browser: true, export: false, server: false },
|
penDetected: { browser: true, export: false, server: false },
|
||||||
errorMessage: { browser: false, export: false, server: false },
|
errorMessage: { browser: false, export: false, server: false },
|
||||||
|
|||||||
+1
-21
@@ -11,8 +11,6 @@ import {
|
|||||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||||
import { NonDeletedExcalidrawElement } from "./element/types";
|
import { NonDeletedExcalidrawElement } from "./element/types";
|
||||||
import { randomId } from "./random";
|
import { randomId } from "./random";
|
||||||
import { AppState } from "./types";
|
|
||||||
import { selectSubtype } from "./element/subtypes";
|
|
||||||
|
|
||||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
@@ -25,8 +23,6 @@ export interface Spreadsheet {
|
|||||||
title: string | null;
|
title: string | null;
|
||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
values: number[];
|
values: number[];
|
||||||
activeSubtypes?: AppState["activeSubtypes"];
|
|
||||||
customData?: AppState["customData"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
||||||
@@ -197,17 +193,13 @@ const chartXLabels = (
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
backgroundColor: string,
|
backgroundColor: string,
|
||||||
): ChartElements => {
|
): ChartElements => {
|
||||||
const custom = selectSubtype(spreadsheet, "text");
|
|
||||||
return (
|
return (
|
||||||
spreadsheet.labels?.map((label, index) => {
|
spreadsheet.labels?.map((label, index) => {
|
||||||
return newTextElement({
|
return newTextElement({
|
||||||
groupIds: [groupId],
|
groupIds: [groupId],
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
...commonProps,
|
...commonProps,
|
||||||
text:
|
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
||||||
label.length > 8 && custom.subtype === undefined
|
|
||||||
? `${label.slice(0, 5)}...`
|
|
||||||
: label,
|
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||||
y: y + BAR_GAP / 2,
|
y: y + BAR_GAP / 2,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
@@ -215,7 +207,6 @@ const chartXLabels = (
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
...custom,
|
|
||||||
});
|
});
|
||||||
}) || []
|
}) || []
|
||||||
);
|
);
|
||||||
@@ -236,7 +227,6 @@ const chartYLabels = (
|
|||||||
y: y - BAR_GAP,
|
y: y - BAR_GAP,
|
||||||
text: "0",
|
text: "0",
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
...selectSubtype(spreadsheet, "text"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxYLabel = newTextElement({
|
const maxYLabel = newTextElement({
|
||||||
@@ -247,7 +237,6 @@ const chartYLabels = (
|
|||||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||||
text: Math.max(...spreadsheet.values).toLocaleString(),
|
text: Math.max(...spreadsheet.values).toLocaleString(),
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
...selectSubtype(spreadsheet, "text"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return [minYLabel, maxYLabel];
|
return [minYLabel, maxYLabel];
|
||||||
@@ -275,7 +264,6 @@ const chartLines = (
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[chartWidth, 0],
|
[chartWidth, 0],
|
||||||
],
|
],
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const yLine = newLinearElement({
|
const yLine = newLinearElement({
|
||||||
@@ -292,7 +280,6 @@ const chartLines = (
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[0, -chartHeight],
|
[0, -chartHeight],
|
||||||
],
|
],
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxLine = newLinearElement({
|
const maxLine = newLinearElement({
|
||||||
@@ -311,7 +298,6 @@ const chartLines = (
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[chartWidth, 0],
|
[chartWidth, 0],
|
||||||
],
|
],
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return [xLine, yLine, maxLine];
|
return [xLine, yLine, maxLine];
|
||||||
@@ -338,7 +324,6 @@ const chartBaseElements = (
|
|||||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
...selectSubtype(spreadsheet, "text"),
|
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -355,7 +340,6 @@ const chartBaseElements = (
|
|||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
opacity: 6,
|
opacity: 6,
|
||||||
...selectSubtype(spreadsheet, "rectangle"),
|
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -388,7 +372,6 @@ const chartTypeBar = (
|
|||||||
y: y - barHeight - BAR_GAP,
|
y: y - barHeight - BAR_GAP,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
height: barHeight,
|
height: barHeight,
|
||||||
...selectSubtype(spreadsheet, "rectangle"),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -441,7 +424,6 @@ const chartTypeLine = (
|
|||||||
width: maxX - minX,
|
width: maxX - minX,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: points as any,
|
points: points as any,
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dots = spreadsheet.values.map((value, index) => {
|
const dots = spreadsheet.values.map((value, index) => {
|
||||||
@@ -458,7 +440,6 @@ const chartTypeLine = (
|
|||||||
y: y + cy - BAR_GAP * 2,
|
y: y + cy - BAR_GAP * 2,
|
||||||
width: BAR_GAP,
|
width: BAR_GAP,
|
||||||
height: BAR_GAP,
|
height: BAR_GAP,
|
||||||
...selectSubtype(spreadsheet, "ellipse"),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -481,7 +462,6 @@ const chartTypeLine = (
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[0, cy],
|
[0, cy],
|
||||||
],
|
],
|
||||||
...selectSubtype(spreadsheet, "line"),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-6
@@ -2,7 +2,7 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { AppState, BinaryFiles } from "./types";
|
import { BinaryFiles } from "./types";
|
||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||||
@@ -167,7 +167,6 @@ export const getSystemClipboard = async (
|
|||||||
export const parseClipboard = async (
|
export const parseClipboard = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent | null,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
appState?: AppState,
|
|
||||||
): Promise<ClipboardData> => {
|
): Promise<ClipboardData> => {
|
||||||
const systemClipboard = await getSystemClipboard(event);
|
const systemClipboard = await getSystemClipboard(event);
|
||||||
|
|
||||||
@@ -187,10 +186,6 @@ export const parseClipboard = async (
|
|||||||
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard);
|
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard);
|
||||||
|
|
||||||
if (spreadsheetResult) {
|
if (spreadsheetResult) {
|
||||||
if ("spreadsheet" in spreadsheetResult) {
|
|
||||||
spreadsheetResult.spreadsheet.activeSubtypes = appState?.activeSubtypes;
|
|
||||||
spreadsheetResult.spreadsheet.customData = appState?.customData;
|
|
||||||
}
|
|
||||||
return spreadsheetResult;
|
return spreadsheetResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-103
@@ -23,7 +23,6 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { SubtypeShapeActions, SubtypeToggles } from "./Subtypes";
|
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { hasBoundTextElement } from "../element/typeChecks";
|
import { hasBoundTextElement } from "../element/typeChecks";
|
||||||
@@ -37,7 +36,12 @@ import {
|
|||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
import {
|
||||||
|
EmbedIcon,
|
||||||
|
extraToolsIcon,
|
||||||
|
frameToolIcon,
|
||||||
|
mermaidLogoIcon,
|
||||||
|
} from "./icons";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
@@ -101,7 +105,6 @@ export const SelectedShapeActions = ({
|
|||||||
{showChangeBackgroundIcons && (
|
{showChangeBackgroundIcons && (
|
||||||
<div>{renderAction("changeBackgroundColor")}</div>
|
<div>{renderAction("changeBackgroundColor")}</div>
|
||||||
)}
|
)}
|
||||||
<SubtypeShapeActions elements={targetElements} />
|
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStrokeWidth(appState.activeTool.type) ||
|
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||||
@@ -228,7 +231,6 @@ export const ShapesSwitcher = ({
|
|||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
}) => {
|
}) => {
|
||||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||||
const device = useDevice();
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
@@ -284,32 +286,22 @@ export const ShapesSwitcher = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="App-toolbar__divider" />
|
<div className="App-toolbar__divider" />
|
||||||
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
|
||||||
{device.isMobile ? (
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
<>
|
<DropdownMenu.Trigger
|
||||||
<ToolButton
|
className="App-toolbar__extra-tools-trigger"
|
||||||
className={clsx("Shape", { fillable: false })}
|
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||||
type="radio"
|
title={t("toolBar.extraTools")}
|
||||||
icon={frameToolIcon}
|
>
|
||||||
checked={activeTool.type === "frame"}
|
{extraToolsIcon}
|
||||||
name="editor-current-shape"
|
</DropdownMenu.Trigger>
|
||||||
title={`${capitalizeString(
|
<DropdownMenu.Content
|
||||||
t("toolBar.frame"),
|
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||||
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||||
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
className="App-toolbar__extra-tools-dropdown"
|
||||||
aria-label={capitalizeString(t("toolBar.frame"))}
|
>
|
||||||
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
<DropdownMenu.Item
|
||||||
data-testid={`toolbar-frame`}
|
onSelect={() => {
|
||||||
onPointerDown={({ pointerType }) => {
|
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
|
||||||
setAppState({
|
|
||||||
penDetected: true,
|
|
||||||
penMode: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={({ pointerType }) => {
|
|
||||||
trackEvent("toolbar", "frame", "ui");
|
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
type: "frame",
|
type: "frame",
|
||||||
});
|
});
|
||||||
@@ -317,29 +309,16 @@ export const ShapesSwitcher = ({
|
|||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
activeEmbeddable: null,
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
icon={frameToolIcon}
|
||||||
<ToolButton
|
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||||
className={clsx("Shape", { fillable: false })}
|
data-testid="toolbar-frame"
|
||||||
type="radio"
|
>
|
||||||
icon={EmbedIcon}
|
{t("toolBar.frame")}
|
||||||
checked={activeTool.type === "embeddable"}
|
</DropdownMenu.Item>
|
||||||
name="editor-current-shape"
|
<DropdownMenu.Item
|
||||||
title={capitalizeString(t("toolBar.embeddable"))}
|
onSelect={() => {
|
||||||
aria-label={capitalizeString(t("toolBar.embeddable"))}
|
|
||||||
data-testid={`toolbar-embeddable`}
|
|
||||||
onPointerDown={({ pointerType }) => {
|
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
|
||||||
setAppState({
|
|
||||||
penDetected: true,
|
|
||||||
penMode: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={({ pointerType }) => {
|
|
||||||
trackEvent("toolbar", "embeddable", "ui");
|
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
type: "embeddable",
|
type: "embeddable",
|
||||||
});
|
});
|
||||||
@@ -347,62 +326,30 @@ export const ShapesSwitcher = ({
|
|||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
activeEmbeddable: null,
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
icon={EmbedIcon}
|
||||||
</>
|
data-testid="toolbar-embeddable"
|
||||||
) : (
|
|
||||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
|
||||||
<DropdownMenu.Trigger
|
|
||||||
className="App-toolbar__extra-tools-trigger"
|
|
||||||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
|
||||||
title={t("toolBar.extraTools")}
|
|
||||||
>
|
>
|
||||||
{extraToolsIcon}
|
{t("toolBar.embeddable")}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Item
|
||||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
onSelect={() => {
|
||||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
className="App-toolbar__extra-tools-dropdown"
|
type: "mermaid",
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
multiElement: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={mermaidLogoIcon}
|
||||||
|
data-testid="toolbar-embeddable"
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
{t("toolBar.mermaidToExcalidraw")}
|
||||||
onSelect={() => {
|
</DropdownMenu.Item>
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
</DropdownMenu.Content>
|
||||||
type: "frame",
|
</DropdownMenu>
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon={frameToolIcon}
|
|
||||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
|
||||||
data-testid="toolbar-frame"
|
|
||||||
>
|
|
||||||
{t("toolBar.frame")}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
|
||||||
type: "embeddable",
|
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon={EmbedIcon}
|
|
||||||
data-testid="toolbar-embeddable"
|
|
||||||
>
|
|
||||||
{t("toolBar.embeddable")}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
<SubtypeToggles />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+60
-69
@@ -270,16 +270,6 @@ import {
|
|||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
import {
|
|
||||||
SubtypeLoadedCb,
|
|
||||||
SubtypeRecord,
|
|
||||||
SubtypePrepFn,
|
|
||||||
checkRefreshOnSubtypeLoad,
|
|
||||||
isSubtypeAction,
|
|
||||||
prepareSubtype,
|
|
||||||
selectSubtype,
|
|
||||||
subtypeActionPredicate,
|
|
||||||
} from "../element/subtypes";
|
|
||||||
import {
|
import {
|
||||||
dataURLToFile,
|
dataURLToFile,
|
||||||
generateIdFromFile,
|
generateIdFromFile,
|
||||||
@@ -364,6 +354,7 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
|||||||
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
||||||
import { Renderer } from "../scene/Renderer";
|
import { Renderer } from "../scene/Renderer";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
import MermaidToExcalidraw from "./MermaidToExcalidraw";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@@ -519,12 +510,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.id = nanoid();
|
this.id = nanoid();
|
||||||
|
|
||||||
this.library = new Library(this);
|
this.library = new Library(this);
|
||||||
this.actionManager = new ActionManager(
|
|
||||||
this.syncActionResult,
|
|
||||||
() => this.state,
|
|
||||||
() => this.scene.getElementsIncludingDeleted(),
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
|
|
||||||
this.canvas = document.createElement("canvas");
|
this.canvas = document.createElement("canvas");
|
||||||
@@ -551,8 +536,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
getSceneElements: this.getSceneElements,
|
getSceneElements: this.getSceneElements,
|
||||||
getAppState: () => this.state,
|
getAppState: () => this.state,
|
||||||
getFiles: () => this.files,
|
getFiles: () => this.files,
|
||||||
actionManager: this.actionManager,
|
|
||||||
addSubtype: this.addSubtype,
|
|
||||||
refresh: this.refresh,
|
refresh: this.refresh,
|
||||||
setToast: this.setToast,
|
setToast: this.setToast,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@@ -580,27 +563,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
onSceneUpdated: this.onSceneUpdated,
|
onSceneUpdated: this.onSceneUpdated,
|
||||||
});
|
});
|
||||||
this.history = new History();
|
this.history = new History();
|
||||||
|
this.actionManager = new ActionManager(
|
||||||
|
this.syncActionResult,
|
||||||
|
() => this.state,
|
||||||
|
() => this.scene.getElementsIncludingDeleted(),
|
||||||
|
this,
|
||||||
|
);
|
||||||
this.actionManager.registerAll(actions);
|
this.actionManager.registerAll(actions);
|
||||||
|
|
||||||
this.actionManager.registerAction(createUndoAction(this.history));
|
this.actionManager.registerAction(createUndoAction(this.history));
|
||||||
this.actionManager.registerAction(createRedoAction(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) {
|
private onWindowMessage(event: MessageEvent) {
|
||||||
@@ -1219,7 +1191,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
app={this}
|
app={this}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
{this.state.activeTool.type === "mermaid" && (
|
||||||
|
<MermaidToExcalidraw
|
||||||
|
selectedElements={this.scene.getSelectedElements(
|
||||||
|
this.state,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</LayerUI>
|
</LayerUI>
|
||||||
|
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<div className="excalidraw-eye-dropper-container" />
|
<div className="excalidraw-eye-dropper-container" />
|
||||||
@@ -2188,7 +2168,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// (something something security)
|
// (something something security)
|
||||||
let file = event?.clipboardData?.files[0];
|
let file = event?.clipboardData?.files[0];
|
||||||
|
|
||||||
const data = await parseClipboard(event, isPlainPaste, this.state);
|
const data = await parseClipboard(event, isPlainPaste);
|
||||||
if (!file && data.text && !isPlainPaste) {
|
if (!file && data.text && !isPlainPaste) {
|
||||||
const string = data.text.trim();
|
const string = data.text.trim();
|
||||||
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
||||||
@@ -2282,11 +2262,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
private addElementsFromPasteOrLibrary = (opts: {
|
addElementsFromPasteOrLibrary = (opts: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
position: { clientX: number; clientY: number } | "cursor" | "center";
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null, undefined);
|
const elements = restoreElements(opts.elements, null, undefined);
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
@@ -2391,6 +2372,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.setActiveTool({ type: "selection" });
|
this.setActiveTool({ type: "selection" });
|
||||||
|
|
||||||
|
if (opts.fitToContent) {
|
||||||
|
this.scrollToContent(newElements, {
|
||||||
|
fitToContent: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private addTextFromPaste(text: string, isPlainPaste = false) {
|
private addTextFromPaste(text: string, isPlainPaste = false) {
|
||||||
@@ -2418,7 +2405,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily: this.state.currentItemFontFamily,
|
||||||
textAlign: this.state.currentItemTextAlign,
|
textAlign: this.state.currentItemTextAlign,
|
||||||
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
||||||
...selectSubtype(this.state, "text"),
|
|
||||||
locked: false,
|
locked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3122,7 +3108,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
private setActiveTool = (
|
public setActiveTool = (
|
||||||
tool:
|
tool:
|
||||||
| {
|
| {
|
||||||
type:
|
type:
|
||||||
@@ -3130,7 +3116,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
| "eraser"
|
| "eraser"
|
||||||
| "hand"
|
| "hand"
|
||||||
| "frame"
|
| "frame"
|
||||||
| "embeddable";
|
| "embeddable"
|
||||||
|
| "mermaid";
|
||||||
}
|
}
|
||||||
| { type: "custom"; customType: string },
|
| { type: "custom"; customType: string },
|
||||||
) => {
|
) => {
|
||||||
@@ -3152,7 +3139,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (nextActiveTool.type !== "selection") {
|
if (nextActiveTool.type !== "selection") {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
nextActiveTool.type === "mermaid"
|
||||||
|
? this.state.selectedElementIds
|
||||||
|
: {},
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
@@ -3555,7 +3547,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
verticalAlign: parentCenterPosition
|
verticalAlign: parentCenterPosition
|
||||||
? VERTICAL_ALIGN.MIDDLE
|
? VERTICAL_ALIGN.MIDDLE
|
||||||
: DEFAULT_VERTICAL_ALIGN,
|
: DEFAULT_VERTICAL_ALIGN,
|
||||||
...selectSubtype(this.state, "text"),
|
|
||||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
lineHeight,
|
lineHeight,
|
||||||
@@ -4089,6 +4080,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
scenePointer.y,
|
scenePointer.y,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.hitLinkElement = this.getElementLinkAtPosition(
|
this.hitLinkElement = this.getElementLinkAtPosition(
|
||||||
scenePointer,
|
scenePointer,
|
||||||
hitElement,
|
hitElement,
|
||||||
@@ -4541,7 +4533,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.createFrameElementOnPointerDown(pointerDownState);
|
this.createFrameElementOnPointerDown(pointerDownState);
|
||||||
} else if (
|
} else if (
|
||||||
this.state.activeTool.type !== "eraser" &&
|
this.state.activeTool.type !== "eraser" &&
|
||||||
this.state.activeTool.type !== "hand"
|
this.state.activeTool.type !== "hand" &&
|
||||||
|
this.state.activeTool.type !== "mermaid"
|
||||||
) {
|
) {
|
||||||
this.createGenericElementOnPointerDown(
|
this.createGenericElementOnPointerDown(
|
||||||
this.state.activeTool.type,
|
this.state.activeTool.type,
|
||||||
@@ -5409,7 +5402,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
roughness: this.state.currentItemRoughness,
|
roughness: this.state.currentItemRoughness,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
...selectSubtype(this.state, "image"),
|
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
@@ -5510,7 +5502,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: null,
|
: null,
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
...selectSubtype(this.state, elementType),
|
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
@@ -5587,7 +5578,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
roughness: this.state.currentItemRoughness,
|
roughness: this.state.currentItemRoughness,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
roundness: this.getCurrentItemRoundness(elementType),
|
roundness: this.getCurrentItemRoundness(elementType),
|
||||||
...selectSubtype(this.state, elementType),
|
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
} as const;
|
} as const;
|
||||||
@@ -7450,6 +7440,30 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ suggestedBindings });
|
this.setState({ suggestedBindings });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setSelection(elements: readonly NonDeletedExcalidrawElement[]) {
|
||||||
|
const selectedElementIds: { [id: string]: true } = {};
|
||||||
|
const selectedGroupIds: { [id: string]: true } = {};
|
||||||
|
|
||||||
|
elements.forEach((ele) => {
|
||||||
|
if (ele.groupIds.length) {
|
||||||
|
selectedElementIds[ele.id] = true;
|
||||||
|
ele.groupIds.forEach((id) => {
|
||||||
|
selectedGroupIds[id] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// exclude bound text elements as we don't mark them as selected when
|
||||||
|
// container is selected unless in group
|
||||||
|
else if (!isBoundToContainer(ele)) {
|
||||||
|
selectedElementIds[ele.id] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
previousSelectedElementIds: this.state.selectedElementIds,
|
||||||
|
selectedElementIds,
|
||||||
|
selectedGroupIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||||
@@ -7908,29 +7922,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private getContextMenuItems = (
|
private getContextMenuItems = (
|
||||||
type: "canvas" | "element",
|
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 => {
|
): ContextMenuItems => {
|
||||||
const options: ContextMenuItems = [];
|
const options: ContextMenuItems = [];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
.excalidraw {
|
||||||
|
.mermaid-to-excalidraw-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
@media screen and (max-width: 860px) {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 20rem;
|
||||||
|
height: 19rem;
|
||||||
|
resize: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 860px) {
|
||||||
|
width: auto;
|
||||||
|
height: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-preview-canvas {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.85rem;
|
||||||
|
|
||||||
|
width: 20rem;
|
||||||
|
height: 19rem;
|
||||||
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
|
||||||
|
left center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e4e4eb;
|
||||||
|
|
||||||
|
@media screen and (max-width: 860px) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-preview-insert.excalidraw-button {
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-weight: 600;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
width: 7.5rem;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
color: $oc-white;
|
||||||
|
|
||||||
|
@media screen and (max-width: 860px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 860px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import { useState, useRef, useEffect, useDeferredValue } from "react";
|
||||||
|
import { BinaryFiles } from "../types";
|
||||||
|
import { useApp } from "./App";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../constants";
|
||||||
|
import {
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
exportToCanvas,
|
||||||
|
} from "../packages/excalidraw/index";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { canvasToBlob } from "../data/blob";
|
||||||
|
import { ArrowRightIcon } from "./icons";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import "./MermaidToExcalidraw.scss";
|
||||||
|
|
||||||
|
import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
|
||||||
|
import type { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import Trans from "./Trans";
|
||||||
|
|
||||||
|
const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw";
|
||||||
|
const MERMAID_EXAMPLE =
|
||||||
|
"flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[test]";
|
||||||
|
|
||||||
|
const saveMermaidDataToStorage = (data: string) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, data);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Unable to access window.localStorage
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importMermaidDataFromStorage = () => {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW);
|
||||||
|
if (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Unable to access localStorage
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorComp = ({ error }: { error: string }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="mermaid-error"
|
||||||
|
style={{
|
||||||
|
color: "red",
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: "30px",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
overflow: "auto",
|
||||||
|
maxHeight: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Error! <p style={{ fontSize: "18px", fontWeight: "600" }}>{error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MermaidToExcalidraw = ({
|
||||||
|
selectedElements,
|
||||||
|
}: {
|
||||||
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
}) => {
|
||||||
|
const mermaidToExcalidrawLib = useRef<{
|
||||||
|
parseMermaidToExcalidraw: (
|
||||||
|
defination: string,
|
||||||
|
options: MermaidOptions,
|
||||||
|
) => Promise<MermaidToExcalidrawResult>;
|
||||||
|
} | null>(null);
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
const deferredText = useDeferredValue(text);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
|
const data = useRef<{
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
files: BinaryFiles | null;
|
||||||
|
}>({ elements: [], files: null });
|
||||||
|
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
|
const resetPreview = () => {
|
||||||
|
const canvasNode = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvasNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parent = canvasNode.parentElement;
|
||||||
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parent.style.background = "";
|
||||||
|
canvasNode.replaceChildren();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMermaidToExcalidrawLib = async () => {
|
||||||
|
mermaidToExcalidrawLib.current = await import(
|
||||||
|
/* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw"
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
loadMermaidToExcalidrawLib();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading) {
|
||||||
|
const selectedMermaidImage = selectedElements.filter(
|
||||||
|
(el) => el.type === "image" && el.customData?.mermaidText,
|
||||||
|
)[0];
|
||||||
|
const data = selectedMermaidImage
|
||||||
|
? selectedMermaidImage.customData?.mermaidText
|
||||||
|
: importMermaidDataFromStorage() || MERMAID_EXAMPLE;
|
||||||
|
|
||||||
|
setText(data);
|
||||||
|
}
|
||||||
|
}, [loading, selectedElements]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const renderExcalidrawPreview = async () => {
|
||||||
|
const canvasNode = canvasRef.current;
|
||||||
|
if (
|
||||||
|
!canvasNode ||
|
||||||
|
!mermaidToExcalidrawLib.current ||
|
||||||
|
deferredText === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { elements, files } =
|
||||||
|
await mermaidToExcalidrawLib.current.parseMermaidToExcalidraw(
|
||||||
|
deferredText,
|
||||||
|
{
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
data.current = {
|
||||||
|
elements: convertToExcalidrawElements(
|
||||||
|
elements.map((el) => {
|
||||||
|
if (el.type === "image") {
|
||||||
|
el.customData = { mermaidText: text };
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
regenerateIds: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
const parent = canvasNode.parentElement!;
|
||||||
|
const maxWidth = parent.offsetWidth;
|
||||||
|
const maxHeight = parent.offsetHeight;
|
||||||
|
let dimension = Math.max(maxWidth, maxHeight);
|
||||||
|
dimension = Math.min(dimension, parent.offsetWidth - 10);
|
||||||
|
dimension = Math.min(dimension, parent.offsetHeight - 10);
|
||||||
|
|
||||||
|
const canvas = await exportToCanvas({
|
||||||
|
elements: data.current.elements,
|
||||||
|
files: data.current.files,
|
||||||
|
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||||
|
maxWidthOrHeight: dimension,
|
||||||
|
});
|
||||||
|
// if converting to blob fails, there's some problem that will
|
||||||
|
// likely prevent preview and export (e.g. canvas too big)
|
||||||
|
await canvasToBlob(canvas);
|
||||||
|
parent.style.background = "#fff";
|
||||||
|
canvasNode.replaceChildren(canvas);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e.message);
|
||||||
|
resetPreview();
|
||||||
|
if (deferredText) {
|
||||||
|
setError(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderExcalidrawPreview();
|
||||||
|
}, [deferredText, text]);
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
app.setActiveTool({ type: "selection" });
|
||||||
|
saveMermaidDataToStorage(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = () => {
|
||||||
|
const { elements: newElements, files } = data.current;
|
||||||
|
app.addElementsFromPasteOrLibrary({
|
||||||
|
elements: newElements,
|
||||||
|
files,
|
||||||
|
position: "center",
|
||||||
|
fitToContent: true,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="dialog-mermaid"
|
||||||
|
onCloseRequest={onClose}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<p style={{ marginBottom: "5px", marginTop: "2px" }}>
|
||||||
|
{t("mermaid.title")}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
style={{ fontSize: "15px", fontStyle: "italic", fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
<Trans
|
||||||
|
i18nKey="mermaid.description"
|
||||||
|
flowchartLink={(el) => (
|
||||||
|
<a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="mermaid-to-excalidraw-wrapper">
|
||||||
|
<div
|
||||||
|
className="mermaid-to-excalidraw-wrapper-text"
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<label>{t("mermaid.syntax")}</label>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
style={{
|
||||||
|
padding: "0.85rem",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid #e4e4eb",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
}}
|
||||||
|
onChange={(event) => setText(event.target.value)}
|
||||||
|
value={text}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mermaid-to-excalidraw-wrapper-preview"
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<label>{t("mermaid.preview")}</label>
|
||||||
|
<div className="mermaid-to-excalidraw-wrapper-preview-canvas">
|
||||||
|
{error && <ErrorComp error={error} />}
|
||||||
|
{loading && <Spinner size="2rem" />}
|
||||||
|
<div ref={canvasRef} />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mermaid-to-excalidraw-wrapper-preview-insert"
|
||||||
|
onSelect={onSelect}
|
||||||
|
>
|
||||||
|
{t("mermaid.button")}
|
||||||
|
<span style={{ paddingLeft: "8px", display: "flex" }}>
|
||||||
|
{ArrowRightIcon}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MermaidToExcalidraw;
|
||||||
@@ -10,12 +10,6 @@ import { useApp } from "./App";
|
|||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
|
|
||||||
import "./PasteChartDialog.scss";
|
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;
|
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||||
|
|
||||||
@@ -31,54 +25,41 @@ const ChartPreviewBtn = (props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
if (!props.spreadsheet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = renderSpreadsheet(
|
||||||
|
props.chartType,
|
||||||
|
props.spreadsheet,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
setChartElements(elements);
|
||||||
let svg: SVGSVGElement;
|
let svg: SVGSVGElement;
|
||||||
const previewNode = previewRef.current!;
|
const previewNode = previewRef.current!;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
(async () => {
|
svg = await exportToSvg(
|
||||||
let elements: ChartElements;
|
elements,
|
||||||
await ensureSubtypesLoaded(
|
{
|
||||||
props.spreadsheet?.activeSubtypes ?? [],
|
exportBackground: false,
|
||||||
() => {
|
viewBackgroundColor: oc.white,
|
||||||
if (!props.spreadsheet) {
|
},
|
||||||
return;
|
null, // files
|
||||||
}
|
);
|
||||||
|
svg.querySelector(".style-fonts")?.remove();
|
||||||
|
previewNode.replaceChildren();
|
||||||
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
elements = renderSpreadsheet(
|
if (props.selected) {
|
||||||
props.chartType,
|
(previewNode.parentNode as HTMLDivElement).focus();
|
||||||
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]);
|
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
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";
|
|
||||||
@@ -165,6 +165,15 @@
|
|||||||
width: var(--lg-button-size);
|
width: var(--lg-button-size);
|
||||||
height: var(--lg-button-size);
|
height: var(--lg-button-size);
|
||||||
|
|
||||||
|
@media screen and (max-width: 450px) {
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 379px) {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: var(--lg-icon-size);
|
width: var(--lg-icon-size);
|
||||||
height: var(--lg-icon-size);
|
height: var(--lg-icon-size);
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
background-color: var(--default-border-color);
|
background-color: var(--default-border-color);
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,5 +38,6 @@
|
|||||||
margin-top: 0.375rem;
|
margin-top: 0.375rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
min-width: 11.875rem;
|
min-width: 11.875rem;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
|
||||||
&--mobile {
|
&--mobile {
|
||||||
bottom: 55px;
|
|
||||||
top: auto;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 0.75rem;
|
row-gap: 0.75rem;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import clsx from "clsx";
|
|||||||
import { Theme } from "../element/types";
|
import { Theme } from "../element/types";
|
||||||
import { THEME } from "../constants";
|
import { THEME } from "../constants";
|
||||||
|
|
||||||
export const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
||||||
|
|
||||||
const handlerColor = (theme: Theme) =>
|
const handlerColor = (theme: Theme) =>
|
||||||
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
||||||
@@ -1653,3 +1653,37 @@ export const frameToolIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const mermaidLogoIcon = createIcon(
|
||||||
|
<path
|
||||||
|
fill="black"
|
||||||
|
d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowRightIcon = createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M4.16602 10H15.8327"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.5 13.3333L15.8333 10"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.5 6.66666L15.8333 9.99999"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 20, height: 20 },
|
||||||
|
);
|
||||||
|
|||||||
+9
-2
@@ -280,6 +280,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
|
.dropdown-menu--mobile {
|
||||||
|
bottom: 55px;
|
||||||
|
top: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-mobile-menu {
|
.App-mobile-menu {
|
||||||
@@ -591,6 +596,8 @@
|
|||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,8 +607,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar--mobile {
|
.App-toolbar--mobile {
|
||||||
overflow-x: auto;
|
overflow: visible;
|
||||||
max-width: 90vw;
|
max-width: 98vw;
|
||||||
|
|
||||||
.ToolIcon__keybinding {
|
.ToolIcon__keybinding {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -97,12 +97,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0.5,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
395,
|
394.5,
|
||||||
35,
|
34.5,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -150,11 +150,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
400,
|
399.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -317,7 +317,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 5,
|
"gap": 205,
|
||||||
},
|
},
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@@ -331,11 +331,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -355,7 +355,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@@ -395,7 +395,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 130,
|
"width": 130,
|
||||||
"x": 340,
|
"x": 240,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -428,11 +428,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -452,7 +452,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@@ -492,7 +492,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 130,
|
"width": 130,
|
||||||
"x": 340,
|
"x": 240,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -562,7 +562,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 555,
|
"x": 355,
|
||||||
"y": 189,
|
"y": 189,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -595,11 +595,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 255,
|
"x": 255,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@@ -659,7 +659,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 130,
|
"width": 130,
|
||||||
"x": 340,
|
"x": 240,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -747,7 +747,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "top",
|
"verticalAlign": "top",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 555,
|
"x": 355,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -801,11 +801,11 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -821,7 +821,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
}
|
}
|
||||||
@@ -846,11 +846,11 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -866,7 +866,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 450,
|
"x": 450,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
}
|
}
|
||||||
@@ -895,7 +895,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
100,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -911,7 +911,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
}
|
}
|
||||||
@@ -940,7 +940,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
100,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -956,7 +956,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 450,
|
"x": 450,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
}
|
}
|
||||||
@@ -1244,11 +1244,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1264,7 +1264,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 100,
|
"y": 100,
|
||||||
}
|
}
|
||||||
@@ -1294,11 +1294,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1314,7 +1314,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 200,
|
"y": 200,
|
||||||
}
|
}
|
||||||
@@ -1344,11 +1344,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1364,7 +1364,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 300,
|
"y": 300,
|
||||||
}
|
}
|
||||||
@@ -1394,11 +1394,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
300,
|
99.5,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -1414,7 +1414,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 300,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 400,
|
"y": 400,
|
||||||
}
|
}
|
||||||
@@ -1454,7 +1454,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 130,
|
"width": 130,
|
||||||
"x": 185,
|
"x": 85,
|
||||||
"y": 87.5,
|
"y": 87.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1493,7 +1493,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 200,
|
"width": 200,
|
||||||
"x": 150,
|
"x": 50,
|
||||||
"y": 187.5,
|
"y": 187.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1533,7 +1533,7 @@ LABELLED ARROW",
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 150,
|
"width": 150,
|
||||||
"x": 175,
|
"x": 75,
|
||||||
"y": 275,
|
"y": 275,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1573,7 +1573,7 @@ LABELLED ARROW",
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 150,
|
"width": 150,
|
||||||
"x": 175,
|
"x": 75,
|
||||||
"y": 375,
|
"y": 375,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
+9
-15
@@ -34,14 +34,13 @@ import {
|
|||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { bumpVersion } from "../element/mutateElement";
|
import { bumpVersion } from "../element/mutateElement";
|
||||||
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
import { getFontString, getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { isValidSubtype } from "../element/subtypes";
|
|
||||||
import { MarkOptional, Mutable } from "../utility-types";
|
import { MarkOptional, Mutable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
measureTextElement,
|
measureBaseline,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "./url";
|
||||||
|
|
||||||
@@ -68,6 +67,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||||||
frame: true,
|
frame: true,
|
||||||
embeddable: true,
|
embeddable: true,
|
||||||
hand: true,
|
hand: true,
|
||||||
|
mermaid: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RestoredDataState = {
|
export type RestoredDataState = {
|
||||||
@@ -93,8 +93,7 @@ const repairBinding = (binding: PointBinding | null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
|
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||||
subtype?: ExcalidrawElement["subtype"];
|
|
||||||
customData?: ExcalidrawElement["customData"];
|
customData?: ExcalidrawElement["customData"];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||||
@@ -160,9 +159,6 @@ const restoreElementWithProperties = <
|
|||||||
locked: element.locked ?? false,
|
locked: element.locked ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if ("subtype" in element && isValidSubtype(element.subtype, base.type)) {
|
|
||||||
base.subtype = element.subtype;
|
|
||||||
}
|
|
||||||
if ("customData" in element) {
|
if ("customData" in element) {
|
||||||
base.customData = element.customData;
|
base.customData = element.customData;
|
||||||
}
|
}
|
||||||
@@ -208,7 +204,11 @@ const restoreElement = (
|
|||||||
: // no element height likely means programmatic use, so default
|
: // no element height likely means programmatic use, so default
|
||||||
// to a fixed line height
|
// to a fixed line height
|
||||||
getDefaultLineHeight(element.fontFamily));
|
getDefaultLineHeight(element.fontFamily));
|
||||||
const baseline = measureTextElement(element, { text }).baseline;
|
const baseline = measureBaseline(
|
||||||
|
element.text,
|
||||||
|
getFontString(element),
|
||||||
|
lineHeight,
|
||||||
|
);
|
||||||
element = restoreElementWithProperties(element, {
|
element = restoreElementWithProperties(element, {
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
@@ -529,12 +529,6 @@ export const restoreAppState = (
|
|||||||
: defaultValue;
|
: defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("activeSubtypes" in appState) {
|
|
||||||
nextAppState.activeSubtypes = appState.activeSubtypes;
|
|
||||||
}
|
|
||||||
if ("customData" in appState) {
|
|
||||||
nextAppState.customData = appState.customData;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...nextAppState,
|
...nextAppState,
|
||||||
cursorButton: localAppState?.cursorButton || "up",
|
cursorButton: localAppState?.cursorButton || "up",
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toMatchObject({
|
expect(text).toMatchObject({
|
||||||
x: 340,
|
x: 240,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "HELLO WORLD!!",
|
text: "HELLO WORLD!!",
|
||||||
@@ -341,7 +341,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(ellipse).toMatchObject({
|
expect(ellipse).toMatchObject({
|
||||||
x: 555,
|
x: 355,
|
||||||
y: 189,
|
y: 189,
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
boundElements: [
|
boundElements: [
|
||||||
@@ -386,7 +386,6 @@ describe("Test Transform", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
|
||||||
const [arrow, text1, text2, text3] = excaldrawElements;
|
const [arrow, text1, text2, text3] = excaldrawElements;
|
||||||
|
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
@@ -406,7 +405,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text1).toMatchObject({
|
expect(text1).toMatchObject({
|
||||||
x: 340,
|
x: 240,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "HELLO WORLD!!",
|
text: "HELLO WORLD!!",
|
||||||
@@ -426,7 +425,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text3).toMatchObject({
|
expect(text3).toMatchObject({
|
||||||
x: 555,
|
x: 355,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
boundElements: [
|
boundElements: [
|
||||||
@@ -603,14 +602,14 @@ describe("Test Transform", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
const [, , arrow] = excaldrawElements;
|
const [, , arrow, text] = excaldrawElements;
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: 255,
|
x: 255,
|
||||||
y: 239,
|
y: 239,
|
||||||
boundElements: [
|
boundElements: [
|
||||||
{
|
{
|
||||||
id: "id46",
|
id: text.id,
|
||||||
type: "text",
|
type: "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -656,11 +655,11 @@ describe("Test Transform", () => {
|
|||||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||||
elementId: "rect-1",
|
elementId: "rect-1",
|
||||||
focus: 0,
|
focus: 0,
|
||||||
gap: 5,
|
gap: 205,
|
||||||
});
|
});
|
||||||
expect(rect.boundElements).toStrictEqual([
|
expect(rect.boundElements).toStrictEqual([
|
||||||
{
|
{
|
||||||
id: "id47",
|
id: arrow.id,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
+85
-4
@@ -39,6 +39,8 @@ import {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { MarkOptional } from "../utility-types";
|
import { MarkOptional } from "../utility-types";
|
||||||
import { assertNever, getFontString } from "../utils";
|
import { assertNever, getFontString } from "../utils";
|
||||||
|
import { getSizeFromPoints } from "../points";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
@@ -159,7 +161,7 @@ export type ExcalidrawElementSkeleton =
|
|||||||
} & Partial<ExcalidrawImageElement>);
|
} & Partial<ExcalidrawImageElement>);
|
||||||
|
|
||||||
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
||||||
width: 300,
|
width: 100,
|
||||||
height: 0,
|
height: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -357,6 +359,48 @@ const bindLinearElementToElement = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.
|
||||||
|
const endPointIndex = linearElement.points.length - 1;
|
||||||
|
const delta = 0.5;
|
||||||
|
const newPoints = JSON.parse(JSON.stringify(linearElement.points));
|
||||||
|
// left to right so shift the arrow towards right
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][0] >
|
||||||
|
linearElement.points[endPointIndex - 1][0]
|
||||||
|
) {
|
||||||
|
newPoints[0][0] = delta;
|
||||||
|
newPoints[endPointIndex][0] -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// right to left so shift the arrow towards left
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][0] <
|
||||||
|
linearElement.points[endPointIndex - 1][0]
|
||||||
|
) {
|
||||||
|
newPoints[0][0] = -delta;
|
||||||
|
newPoints[endPointIndex][0] += delta;
|
||||||
|
}
|
||||||
|
// top to bottom so shift the arrow towards top
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][1] >
|
||||||
|
linearElement.points[endPointIndex - 1][1]
|
||||||
|
) {
|
||||||
|
newPoints[0][1] = delta;
|
||||||
|
newPoints[endPointIndex][1] -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom to top so shift the arrow towards bottom
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][1] <
|
||||||
|
linearElement.points[endPointIndex - 1][1]
|
||||||
|
) {
|
||||||
|
newPoints[0][1] = -delta;
|
||||||
|
newPoints[endPointIndex][1] += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(linearElement, { points: newPoints });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linearElement,
|
linearElement,
|
||||||
startBoundElement,
|
startBoundElement,
|
||||||
@@ -367,7 +411,7 @@ const bindLinearElementToElement = (
|
|||||||
class ElementStore {
|
class ElementStore {
|
||||||
excalidrawElements = new Map<string, ExcalidrawElement>();
|
excalidrawElements = new Map<string, ExcalidrawElement>();
|
||||||
|
|
||||||
add = (ele?: ExcalidrawElement) => {
|
add = (ele?: ExcalidrawElement, originalId?: string) => {
|
||||||
if (!ele) {
|
if (!ele) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -385,6 +429,7 @@ class ElementStore {
|
|||||||
|
|
||||||
export const convertToExcalidrawElements = (
|
export const convertToExcalidrawElements = (
|
||||||
elements: ExcalidrawElementSkeleton[] | null,
|
elements: ExcalidrawElementSkeleton[] | null,
|
||||||
|
opts?: { regenerateIds: boolean },
|
||||||
) => {
|
) => {
|
||||||
if (!elements) {
|
if (!elements) {
|
||||||
return [];
|
return [];
|
||||||
@@ -392,10 +437,16 @@ export const convertToExcalidrawElements = (
|
|||||||
|
|
||||||
const elementStore = new ElementStore();
|
const elementStore = new ElementStore();
|
||||||
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
||||||
|
const oldToNewElementIdMap = new Map<string, string>();
|
||||||
|
|
||||||
// Create individual elements
|
// Create individual elements
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
let excalidrawElement: ExcalidrawElement;
|
let excalidrawElement: ExcalidrawElement;
|
||||||
|
const originalId = element.id;
|
||||||
|
if (opts?.regenerateIds) {
|
||||||
|
Object.assign(element, { id: nanoid() });
|
||||||
|
}
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
@@ -444,6 +495,11 @@ export const convertToExcalidrawElements = (
|
|||||||
],
|
],
|
||||||
...element,
|
...element,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
excalidrawElement,
|
||||||
|
getSizeFromPoints(excalidrawElement.points),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
@@ -499,6 +555,9 @@ export const convertToExcalidrawElements = (
|
|||||||
} else {
|
} else {
|
||||||
elementStore.add(excalidrawElement);
|
elementStore.add(excalidrawElement);
|
||||||
elementsWithIds.set(excalidrawElement.id, element);
|
elementsWithIds.set(excalidrawElement.id, element);
|
||||||
|
if (originalId) {
|
||||||
|
oldToNewElementIdMap.set(originalId, excalidrawElement.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +583,18 @@ export const convertToExcalidrawElements = (
|
|||||||
element.type === "arrow" ? element?.start : undefined;
|
element.type === "arrow" ? element?.start : undefined;
|
||||||
const originalEnd =
|
const originalEnd =
|
||||||
element.type === "arrow" ? element?.end : undefined;
|
element.type === "arrow" ? element?.end : undefined;
|
||||||
|
if (originalStart && originalStart.id) {
|
||||||
|
const newStartId = oldToNewElementIdMap.get(originalStart.id);
|
||||||
|
if (newStartId) {
|
||||||
|
Object.assign(originalStart, { id: newStartId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (originalEnd && originalEnd.id) {
|
||||||
|
const newEndId = oldToNewElementIdMap.get(originalEnd.id);
|
||||||
|
if (newEndId) {
|
||||||
|
Object.assign(originalEnd, { id: newEndId });
|
||||||
|
}
|
||||||
|
}
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
bindLinearElementToElement(
|
bindLinearElementToElement(
|
||||||
container as ExcalidrawArrowElement,
|
container as ExcalidrawArrowElement,
|
||||||
@@ -539,13 +610,23 @@ export const convertToExcalidrawElements = (
|
|||||||
} else {
|
} else {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
|
const { start, end } = element;
|
||||||
|
if (start && start.id) {
|
||||||
|
const newStartId = oldToNewElementIdMap.get(start.id);
|
||||||
|
Object.assign(start, { id: newStartId });
|
||||||
|
}
|
||||||
|
if (end && end.id) {
|
||||||
|
const newEndId = oldToNewElementIdMap.get(end.id);
|
||||||
|
Object.assign(end, { id: newEndId });
|
||||||
|
}
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
bindLinearElementToElement(
|
bindLinearElementToElement(
|
||||||
excalidrawElement as ExcalidrawArrowElement,
|
excalidrawElement as ExcalidrawArrowElement,
|
||||||
element.start,
|
start,
|
||||||
element.end,
|
end,
|
||||||
elementStore,
|
elementStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
elementStore.add(linearElement);
|
elementStore.add(linearElement);
|
||||||
elementStore.add(startBoundElement);
|
elementStore.add(startBoundElement);
|
||||||
elementStore.add(endBoundElement);
|
elementStore.add(endBoundElement);
|
||||||
|
|||||||
+12
-11
@@ -7,7 +7,11 @@ import { AppState, PointerDownState } from "../types";
|
|||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { isSelectedViaGroup } from "../groups";
|
import { isSelectedViaGroup } from "../groups";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isFrameElement } from "./typeChecks";
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isBoundToContainer,
|
||||||
|
isFrameElement,
|
||||||
|
} from "./typeChecks";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
@@ -36,6 +40,7 @@ export const dragSelectedElements = (
|
|||||||
if (frames.length > 0) {
|
if (frames.length > 0) {
|
||||||
const elementsInFrames = scene
|
const elementsInFrames = scene
|
||||||
.getNonDeletedElements()
|
.getNonDeletedElements()
|
||||||
|
.filter((e) => !isBoundToContainer(e))
|
||||||
.filter((e) => e.frameId !== null)
|
.filter((e) => e.frameId !== null)
|
||||||
.filter((e) => frames.includes(e.frameId!));
|
.filter((e) => frames.includes(e.frameId!));
|
||||||
|
|
||||||
@@ -54,20 +59,16 @@ export const dragSelectedElements = (
|
|||||||
// update coords of bound text only if we're dragging the container directly
|
// update coords of bound text only if we're dragging the container directly
|
||||||
// (we don't drag the group that it's part of)
|
// (we don't drag the group that it's part of)
|
||||||
if (
|
if (
|
||||||
|
// Don't update coords of arrow label since we calculate its position during render
|
||||||
|
!isArrowElement(element) &&
|
||||||
// container isn't part of any group
|
// container isn't part of any group
|
||||||
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
||||||
!element.groupIds.length ||
|
(!element.groupIds.length ||
|
||||||
// container is part of a group, but we're dragging the container directly
|
// container is part of a group, but we're dragging the container directly
|
||||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
(appState.editingGroupId && !isSelectedViaGroup(appState, element)))
|
||||||
) {
|
) {
|
||||||
const textElement = getBoundTextElement(element);
|
const textElement = getBoundTextElement(element);
|
||||||
if (
|
if (textElement) {
|
||||||
textElement &&
|
|
||||||
// when container is added to a frame, so will its bound text
|
|
||||||
// so the text is already in `elementsToUpdate` and we should avoid
|
|
||||||
// updating its coords again
|
|
||||||
(!textElement.frameId || !frames.includes(textElement.frameId))
|
|
||||||
) {
|
|
||||||
updateElementCoords(
|
updateElementCoords(
|
||||||
lockDirection,
|
lockDirection,
|
||||||
distanceX,
|
distanceX,
|
||||||
|
|||||||
@@ -6,23 +6,12 @@ import { Point } from "../types";
|
|||||||
import { getUpdatedTimestamp } from "../utils";
|
import { getUpdatedTimestamp } from "../utils";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { maybeGetSubtypeProps } from "./newElement";
|
|
||||||
import { getSubtypeMethods } from "./subtypes";
|
|
||||||
|
|
||||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
"id" | "version" | "versionNonce"
|
"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.
|
// 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 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
|
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||||
@@ -33,8 +22,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
informMutation = true,
|
informMutation = true,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
let increment = false;
|
|
||||||
const oldUpdates = cleanUpdates(element, updates);
|
|
||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
@@ -83,7 +70,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!didChangePoints) {
|
if (!didChangePoints) {
|
||||||
key in oldUpdates && (increment = true);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +77,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
|
|
||||||
(element as any)[key] = value;
|
(element as any)[key] = value;
|
||||||
didChange = true;
|
didChange = true;
|
||||||
key in oldUpdates && (increment = true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!didChange) {
|
if (!didChange) {
|
||||||
@@ -107,11 +92,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (increment) {
|
element.version++;
|
||||||
element.version++;
|
element.versionNonce = randomInteger();
|
||||||
element.versionNonce = randomInteger();
|
element.updated = getUpdatedTimestamp();
|
||||||
element.updated = getUpdatedTimestamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (informMutation) {
|
if (informMutation) {
|
||||||
Scene.getScene(element)?.informMutation();
|
Scene.getScene(element)?.informMutation();
|
||||||
@@ -125,8 +108,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
let increment = false;
|
|
||||||
const oldUpdates = cleanUpdates(element, updates);
|
|
||||||
for (const key in updates) {
|
for (const key in updates) {
|
||||||
const value = (updates as any)[key];
|
const value = (updates as any)[key];
|
||||||
if (typeof value !== "undefined") {
|
if (typeof value !== "undefined") {
|
||||||
@@ -138,7 +119,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
didChange = true;
|
didChange = true;
|
||||||
key in oldUpdates && (increment = true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,9 +126,6 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!increment) {
|
|
||||||
return { ...element, ...updates };
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
...updates,
|
...updates,
|
||||||
|
|||||||
+24
-54
@@ -15,7 +15,12 @@ import {
|
|||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
ExcalidrawEmbeddableElement,
|
ExcalidrawEmbeddableElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
|
import {
|
||||||
|
arrayToMap,
|
||||||
|
getFontString,
|
||||||
|
getUpdatedTimestamp,
|
||||||
|
isTestEnv,
|
||||||
|
} from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
import { bumpVersion, newElementWith } from "./mutateElement";
|
import { bumpVersion, newElementWith } from "./mutateElement";
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
@@ -25,9 +30,9 @@ import { adjustXYWithRotation } from "../math";
|
|||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
measureTextElement,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapTextElement,
|
wrapText,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
@@ -40,30 +45,6 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
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<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
@@ -77,8 +58,6 @@ export type ElementConstructorOpts = MarkOptional<
|
|||||||
| "version"
|
| "version"
|
||||||
| "versionNonce"
|
| "versionNonce"
|
||||||
| "link"
|
| "link"
|
||||||
| "subtype"
|
|
||||||
| "customData"
|
|
||||||
| "strokeStyle"
|
| "strokeStyle"
|
||||||
| "fillStyle"
|
| "fillStyle"
|
||||||
| "strokeColor"
|
| "strokeColor"
|
||||||
@@ -114,10 +93,8 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
...rest
|
...rest
|
||||||
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
||||||
) => {
|
) => {
|
||||||
const { subtype, customData } = rest;
|
|
||||||
// assign type to guard against excess properties
|
// assign type to guard against excess properties
|
||||||
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
||||||
...maybeGetSubtypeProps({ subtype, customData }, type),
|
|
||||||
id: rest.id || randomId(),
|
id: rest.id || randomId(),
|
||||||
type,
|
type,
|
||||||
x,
|
x,
|
||||||
@@ -151,11 +128,8 @@ export const newElement = (
|
|||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawGenericElement["type"];
|
type: ExcalidrawGenericElement["type"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawGenericElement> => {
|
): NonDeleted<ExcalidrawGenericElement> =>
|
||||||
const map = getSubtypeMethods(opts?.subtype);
|
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||||
map?.clean && map.clean(opts);
|
|
||||||
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newEmbeddableElement = (
|
export const newEmbeddableElement = (
|
||||||
opts: {
|
opts: {
|
||||||
@@ -222,12 +196,10 @@ export const newTextElement = (
|
|||||||
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
||||||
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
|
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
|
||||||
const text = normalizeText(opts.text);
|
const text = normalizeText(opts.text);
|
||||||
const metrics = measureTextElement(
|
const metrics = measureText(
|
||||||
{ ...opts, fontSize, fontFamily, lineHeight },
|
text,
|
||||||
{
|
getFontString({ fontFamily, fontSize }),
|
||||||
text,
|
lineHeight,
|
||||||
customData: opts.customData,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
||||||
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
||||||
@@ -272,9 +244,7 @@ const getAdjustedDimensions = (
|
|||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
baseline: nextBaseline,
|
baseline: nextBaseline,
|
||||||
} = measureTextElement(element, {
|
} = measureText(nextText, getFontString(element), element.lineHeight);
|
||||||
text: nextText,
|
|
||||||
});
|
|
||||||
const { textAlign, verticalAlign } = element;
|
const { textAlign, verticalAlign } = element;
|
||||||
let x: number;
|
let x: number;
|
||||||
let y: number;
|
let y: number;
|
||||||
@@ -283,7 +253,11 @@ const getAdjustedDimensions = (
|
|||||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||||
!element.containerId
|
!element.containerId
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureTextElement(element);
|
const prevMetrics = measureText(
|
||||||
|
element.text,
|
||||||
|
getFontString(element),
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
const offsets = getTextElementPositionOffsets(element, {
|
const offsets = getTextElementPositionOffsets(element, {
|
||||||
width: nextWidth - prevMetrics.width,
|
width: nextWidth - prevMetrics.width,
|
||||||
height: nextHeight - prevMetrics.height,
|
height: nextHeight - prevMetrics.height,
|
||||||
@@ -339,9 +313,11 @@ export const refreshTextDimensions = (
|
|||||||
}
|
}
|
||||||
const container = getContainerElement(textElement);
|
const container = getContainerElement(textElement);
|
||||||
if (container) {
|
if (container) {
|
||||||
text = wrapTextElement(textElement, getBoundTextMaxWidth(container), {
|
text = wrapText(
|
||||||
text,
|
text,
|
||||||
});
|
getFontString(textElement),
|
||||||
|
getBoundTextMaxWidth(container),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const dimensions = getAdjustedDimensions(textElement, text);
|
const dimensions = getAdjustedDimensions(textElement, text);
|
||||||
return { text, ...dimensions };
|
return { text, ...dimensions };
|
||||||
@@ -373,8 +349,6 @@ export const newFreeDrawElement = (
|
|||||||
simulatePressure: boolean;
|
simulatePressure: boolean;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
||||||
const map = getSubtypeMethods(opts?.subtype);
|
|
||||||
map?.clean && map.clean(opts);
|
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
@@ -392,8 +366,6 @@ export const newLinearElement = (
|
|||||||
points?: ExcalidrawLinearElement["points"];
|
points?: ExcalidrawLinearElement["points"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawLinearElement> => {
|
): NonDeleted<ExcalidrawLinearElement> => {
|
||||||
const map = getSubtypeMethods(opts?.subtype);
|
|
||||||
map?.clean && map.clean(opts);
|
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
@@ -413,8 +385,6 @@ export const newImageElement = (
|
|||||||
scale?: ExcalidrawImageElement["scale"];
|
scale?: ExcalidrawImageElement["scale"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawImageElement> => {
|
): NonDeleted<ExcalidrawImageElement> => {
|
||||||
const map = getSubtypeMethods(opts?.subtype);
|
|
||||||
map?.clean && map.clean(opts);
|
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawImageElement>("image", opts),
|
..._newElementBase<ExcalidrawImageElement>("image", opts),
|
||||||
// in the future we'll support changing stroke color for some SVG elements,
|
// in the future we'll support changing stroke color for some SVG elements,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import {
|
|||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
measureTextElement,
|
measureText,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
@@ -223,7 +223,11 @@ const measureFontSizeFromWidth = (
|
|||||||
if (nextFontSize < MIN_FONT_SIZE) {
|
if (nextFontSize < MIN_FONT_SIZE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const metrics = measureTextElement(element, { fontSize: nextFontSize });
|
const metrics = measureText(
|
||||||
|
element.text,
|
||||||
|
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
size: nextFontSize,
|
size: nextFontSize,
|
||||||
baseline: metrics.baseline + (nextHeight - metrics.height),
|
baseline: metrics.baseline + (nextHeight - metrics.height),
|
||||||
|
|||||||
@@ -1,490 +0,0 @@
|
|||||||
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]);
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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 },
|
|
||||||
);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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"],
|
|
||||||
};
|
|
||||||
+20
-39
@@ -1,4 +1,3 @@
|
|||||||
import { getSubtypeMethods, SubtypeMethods } from "./subtypes";
|
|
||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -37,30 +36,6 @@ import {
|
|||||||
} from "./textWysiwyg";
|
} from "./textWysiwyg";
|
||||||
import { ExtractSetType } from "../utility-types";
|
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) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
text
|
text
|
||||||
@@ -93,24 +68,22 @@ export const redrawTextBoundingBox = (
|
|||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
maxWidth = getBoundTextMaxWidth(container, textElement);
|
maxWidth = getBoundTextMaxWidth(container, textElement);
|
||||||
boundTextUpdates.text = wrapTextElement(textElement, maxWidth);
|
boundTextUpdates.text = wrapText(
|
||||||
|
textElement.originalText,
|
||||||
|
getFontString(textElement),
|
||||||
|
maxWidth,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const metrics = measureTextElement(textElement, {
|
const metrics = measureText(
|
||||||
text: boundTextUpdates.text,
|
boundTextUpdates.text,
|
||||||
});
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
boundTextUpdates.width = metrics.width;
|
boundTextUpdates.width = metrics.width;
|
||||||
boundTextUpdates.height = metrics.height;
|
boundTextUpdates.height = metrics.height;
|
||||||
boundTextUpdates.baseline = metrics.baseline;
|
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) {
|
if (container) {
|
||||||
const maxContainerHeight = getBoundTextMaxHeight(
|
const maxContainerHeight = getBoundTextMaxHeight(
|
||||||
container,
|
container,
|
||||||
@@ -223,9 +196,17 @@ export const handleBindTextResize = (
|
|||||||
(transformHandleType !== "n" && transformHandleType !== "s")
|
(transformHandleType !== "n" && transformHandleType !== "s")
|
||||||
) {
|
) {
|
||||||
if (text) {
|
if (text) {
|
||||||
text = wrapTextElement(textElement, maxWidth);
|
text = wrapText(
|
||||||
|
textElement.originalText,
|
||||||
|
getFontString(textElement),
|
||||||
|
maxWidth,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const metrics = measureTextElement(textElement, { text });
|
const metrics = measureText(
|
||||||
|
text,
|
||||||
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
);
|
||||||
nextHeight = metrics.height;
|
nextHeight = metrics.height;
|
||||||
nextWidth = metrics.width;
|
nextWidth = metrics.width;
|
||||||
nextBaseLine = metrics.baseline;
|
nextBaseLine = metrics.baseline;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { Excalidraw } from "../packages/excalidraw/index";
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
import {
|
||||||
|
GlobalTestState,
|
||||||
|
getTextEditor,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
updateTextEditor,
|
||||||
|
} from "../tests/test-utils";
|
||||||
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import {
|
import {
|
||||||
@@ -26,16 +32,7 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
|||||||
const tab = " ";
|
const tab = " ";
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
const getTextEditor = () => {
|
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
||||||
return document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
|
|
||||||
fireEvent.change(editor, { target: { value } });
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("textWysiwyg", () => {
|
describe("textWysiwyg", () => {
|
||||||
describe("start text editing", () => {
|
describe("start text editing", () => {
|
||||||
@@ -201,7 +198,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
expect(h.state.editingElement?.id).toBe(text.id);
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
@@ -223,7 +220,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
expect(h.state.editingElement?.id).toBe(text.id);
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
@@ -250,7 +247,7 @@ describe("textWysiwyg", () => {
|
|||||||
textElement = UI.createElement("text");
|
textElement = UI.createElement("text");
|
||||||
|
|
||||||
mouse.clickOn(textElement);
|
mouse.clickOn(textElement);
|
||||||
textarea = getTextEditor();
|
textarea = getTextEditor(textEditorSelector);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -460,7 +457,7 @@ describe("textWysiwyg", () => {
|
|||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(750, 300);
|
mouse.clickAt(750, 300);
|
||||||
|
|
||||||
textarea = getTextEditor();
|
textarea = getTextEditor(textEditorSelector);
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
textarea,
|
textarea,
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||||
@@ -512,7 +509,7 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -540,7 +537,7 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
expect(text.angle).toBe(rectangle.angle);
|
expect(text.angle).toBe(rectangle.angle);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -567,7 +564,7 @@ describe("textWysiwyg", () => {
|
|||||||
API.setSelectedElements([diamond]);
|
API.setSelectedElements([diamond]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
const value = new Array(1000).fill("1").join("\n");
|
const value = new Array(1000).fill("1").join("\n");
|
||||||
@@ -602,7 +599,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
let editor = getTextEditor();
|
let editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -617,7 +614,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -639,7 +636,7 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -674,7 +671,7 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -699,7 +696,7 @@ describe("textWysiwyg", () => {
|
|||||||
freedraw.y + freedraw.height / 2,
|
freedraw.y + freedraw.height / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
|
||||||
@@ -733,7 +730,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -748,7 +745,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
editor,
|
editor,
|
||||||
@@ -793,7 +790,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.down();
|
mouse.down();
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = getTextEditor();
|
let editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
@@ -806,7 +803,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
@@ -839,7 +836,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
Keyboard.keyDown(KEYS.ENTER);
|
Keyboard.keyDown(KEYS.ENTER);
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = getTextEditor();
|
let editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -860,7 +857,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -889,7 +886,7 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -926,7 +923,7 @@ describe("textWysiwyg", () => {
|
|||||||
// Bind first text
|
// Bind first text
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -947,7 +944,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should respect text alignment when resizing", async () => {
|
it("should respect text alignment when resizing", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
let editor = getTextEditor();
|
let editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -964,7 +961,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -987,7 +984,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -1025,7 +1022,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -1040,7 +1037,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should scale font size correctly when resizing using shift", async () => {
|
it("should scale font size correctly when resizing using shift", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1060,7 +1057,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1092,7 +1089,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("undo should work", async () => {
|
it("undo should work", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1129,7 +1126,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("should not allow bound text with only whitespaces", async () => {
|
it("should not allow bound text with only whitespaces", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
updateTextEditor(editor, " ");
|
updateTextEditor(editor, " ");
|
||||||
@@ -1184,7 +1181,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should reset the container height cache when resizing", async () => {
|
it("should reset the container height cache when resizing", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
let editor = getTextEditor();
|
let editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1196,7 +1193,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1209,7 +1206,7 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -1234,7 +1231,7 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(
|
expect(
|
||||||
@@ -1266,12 +1263,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
editor.select();
|
editor.select();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1382,7 +1379,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = getTextEditor();
|
const editor = getTextEditor(textEditorSelector);
|
||||||
|
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
editor,
|
editor,
|
||||||
@@ -1470,7 +1467,7 @@ describe("textWysiwyg", () => {
|
|||||||
// Bind first text
|
// Bind first text
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
let editor = getTextEditor();
|
let editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello!");
|
updateTextEditor(editor, "Hello!");
|
||||||
expect(
|
expect(
|
||||||
@@ -1495,7 +1492,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Excalidraw");
|
updateTextEditor(editor, "Excalidraw");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1519,7 +1516,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.select(arrow);
|
mouse.select(arrow);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
let editor = getTextEditor();
|
let editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1528,7 +1525,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.select(arrow);
|
mouse.select(arrow);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = getTextEditor();
|
editor = getTextEditor(textEditorSelector);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello\nworld!");
|
updateTextEditor(editor, "Hello\nworld!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
getTextWidth,
|
||||||
measureText,
|
|
||||||
normalizeText,
|
normalizeText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
wrapText,
|
||||||
@@ -44,10 +43,8 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
|||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
import { SubtypeMethods, getSubtypeMethods } from "./subtypes";
|
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
offsetX: number,
|
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
angle: number,
|
angle: number,
|
||||||
@@ -65,8 +62,7 @@ const getTransform = (
|
|||||||
if (height > maxHeight && zoom.value !== 1) {
|
if (height > maxHeight && zoom.value !== 1) {
|
||||||
translateY = (maxHeight * (zoom.value - 1)) / 2;
|
translateY = (maxHeight * (zoom.value - 1)) / 2;
|
||||||
}
|
}
|
||||||
const offset = offsetX !== 0 ? ` translate(${offsetX}px, 0px)` : "";
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)${offset}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalContainerCache: {
|
const originalContainerCache: {
|
||||||
@@ -101,14 +97,6 @@ export const getOriginalContainerHeightFromCache = (
|
|||||||
return originalContainerCache[id]?.height ?? null;
|
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 = ({
|
export const textWysiwyg = ({
|
||||||
id,
|
id,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -168,24 +156,11 @@ export const textWysiwyg = ({
|
|||||||
const container = getContainerElement(updatedTextElement);
|
const container = getContainerElement(updatedTextElement);
|
||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = updatedTextElement.width;
|
||||||
|
|
||||||
// Editing metrics
|
let maxHeight = updatedTextElement.height;
|
||||||
const eMetrics = measureText(
|
let textElementWidth = updatedTextElement.width;
|
||||||
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
|
// Set to element height by default since that's
|
||||||
// what is going to be used for unbounded text
|
// what is going to be used for unbounded text
|
||||||
const textElementHeight = Math.max(updatedTextElement.height, maxHeight);
|
const textElementHeight = updatedTextElement.height;
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
@@ -271,35 +246,13 @@ export const textWysiwyg = ({
|
|||||||
editable.selectionEnd = editable.value.length - diff;
|
editable.selectionEnd = editable.value.length - diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transformWidth = updatedTextElement.width;
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
textElementWidth = Math.min(textElementWidth, maxWidth);
|
textElementWidth = Math.min(textElementWidth, maxWidth);
|
||||||
} else {
|
} else {
|
||||||
textElementWidth += 0.5;
|
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;
|
let lineHeight = updatedTextElement.lineHeight;
|
||||||
|
|
||||||
// In Safari the font size gets rounded off when rendering hence calculating the line height by rounding off font size
|
// In Safari the font size gets rounded off when rendering hence calculating the line height by rounding off font size
|
||||||
@@ -317,15 +270,13 @@ export const textWysiwyg = ({
|
|||||||
font: getFontString(updatedTextElement),
|
font: getFontString(updatedTextElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight,
|
lineHeight,
|
||||||
width: `${Math.min(textElementWidth, maxWidth)}px`,
|
width: `${textElementWidth}px`,
|
||||||
height: `${textElementHeight}px`,
|
height: `${textElementHeight}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX}px`,
|
||||||
top: `${viewportY}px`,
|
top: `${viewportY}px`,
|
||||||
...transformOrigin,
|
|
||||||
transform: getTransform(
|
transform: getTransform(
|
||||||
offsetX,
|
textElementWidth,
|
||||||
transformWidth,
|
textElementHeight,
|
||||||
updatedTextElement.height,
|
|
||||||
getTextElementAngle(updatedTextElement),
|
getTextElementAngle(updatedTextElement),
|
||||||
appState,
|
appState,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
@@ -383,7 +334,6 @@ export const textWysiwyg = ({
|
|||||||
whiteSpace,
|
whiteSpace,
|
||||||
overflowWrap: "break-word",
|
overflowWrap: "break-word",
|
||||||
boxSizing: "content-box",
|
boxSizing: "content-box",
|
||||||
...getEditorStyle(element),
|
|
||||||
});
|
});
|
||||||
editable.value = element.originalText;
|
editable.value = element.originalText;
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ type _ExcalidrawElementBase = Readonly<{
|
|||||||
updated: number;
|
updated: number;
|
||||||
link: string | null;
|
link: string | null;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
subtype?: string;
|
|
||||||
customData?: Record<string, any>;
|
customData?: Record<string, any>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
Vendored
-2
@@ -116,5 +116,3 @@ declare namespace jest {
|
|||||||
toBeNonNaNNumber(): void;
|
toBeNonNaNNumber(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "mathjax-full/mjs/input/asciimath/legacy/MathJax";
|
|
||||||
|
|||||||
+1
-37
@@ -87,22 +87,6 @@ if (import.meta.env.DEV) {
|
|||||||
let currentLang: Language = defaultLang;
|
let currentLang: Language = defaultLang;
|
||||||
let currentLangData = {};
|
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) => {
|
export const setLanguage = async (lang: Language) => {
|
||||||
currentLang = lang;
|
currentLang = lang;
|
||||||
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
||||||
@@ -115,17 +99,6 @@ export const setLanguage = async (lang: Language) => {
|
|||||||
currentLangData = await import(
|
currentLangData = await import(
|
||||||
/* webpackChunkName: "locales/[request]" */ `./locales/${currentLang.code}.json`
|
/* 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) {
|
} catch (error: any) {
|
||||||
console.error(`Failed to load language ${lang.code}:`, error.message);
|
console.error(`Failed to load language ${lang.code}:`, error.message);
|
||||||
currentLangData = fallbackLangData;
|
currentLangData = fallbackLangData;
|
||||||
@@ -152,9 +125,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const t = (
|
export const t = (
|
||||||
path:
|
path: NestedKeyOf<typeof fallbackLangData>,
|
||||||
| NestedKeyOf<typeof fallbackLangData>
|
|
||||||
| `${NestedKeyOf<typeof fallbackLangData>}.${string}`,
|
|
||||||
replacement?: { [key: string]: string | number } | null,
|
replacement?: { [key: string]: string | number } | null,
|
||||||
fallback?: string,
|
fallback?: string,
|
||||||
) => {
|
) => {
|
||||||
@@ -170,13 +141,6 @@ export const t = (
|
|||||||
findPartsForData(currentLangData, parts) ||
|
findPartsForData(currentLangData, parts) ||
|
||||||
findPartsForData(fallbackLangData, parts) ||
|
findPartsForData(fallbackLangData, parts) ||
|
||||||
fallback;
|
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) {
|
if (translation === undefined) {
|
||||||
const errorMessage = `Can't find translation for ${path}`;
|
const errorMessage = `Can't find translation for ${path}`;
|
||||||
// in production, don't blow up the app on a missing translation key
|
// in production, don't blow up the app on a missing translation key
|
||||||
|
|||||||
+9
-1
@@ -236,7 +236,8 @@
|
|||||||
"frame": "Frame tool",
|
"frame": "Frame tool",
|
||||||
"embeddable": "Web Embed",
|
"embeddable": "Web Embed",
|
||||||
"hand": "Hand (panning tool)",
|
"hand": "Hand (panning tool)",
|
||||||
"extraTools": "More tools"
|
"extraTools": "More tools",
|
||||||
|
"mermaidToExcalidraw": "Mermaid to Excalidraw"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas actions",
|
"canvasActions": "Canvas actions",
|
||||||
@@ -495,5 +496,12 @@
|
|||||||
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
|
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"mermaid": {
|
||||||
|
"title": "Mermaid to Excalidraw",
|
||||||
|
"button": "Insert",
|
||||||
|
"description": "Currently only <flowchartLink>flowcharts</flowchartLink> are supported. The other types will be rendered as image in Excalidraw.",
|
||||||
|
"syntax": "Mermaid Syntax",
|
||||||
|
"preview": "Preview"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
## 0.16.0 (2023-09-19)
|
## 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)
|
- 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)
|
- 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)
|
- 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)
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ module.exports = {
|
|||||||
"sass-loader",
|
"sass-loader",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// So that type module works with webpack
|
||||||
|
// https://github.com/webpack/webpack/issues/11467#issuecomment-691873586
|
||||||
|
{
|
||||||
|
test: /\.m?js/,
|
||||||
|
resolve: {
|
||||||
|
fullySpecified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude:
|
exclude:
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ module.exports = {
|
|||||||
"sass-loader",
|
"sass-loader",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// So that type module works with webpack
|
||||||
|
// https://github.com/webpack/webpack/issues/11467#issuecomment-691873586
|
||||||
|
{
|
||||||
|
test: /\.m?js/,
|
||||||
|
resolve: {
|
||||||
|
fullySpecified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude:
|
exclude:
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { getSubtypeMethods } from "../element/subtypes";
|
|
||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
@@ -265,12 +264,6 @@ const drawElementOnCanvas = (
|
|||||||
) => {
|
) => {
|
||||||
context.globalAlpha =
|
context.globalAlpha =
|
||||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
((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) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
@@ -904,11 +897,6 @@ export const renderElementToSvg = (
|
|||||||
root = anchorTag;
|
root = anchorTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
const map = getSubtypeMethods(element.subtype);
|
|
||||||
if (map?.renderSvg) {
|
|
||||||
map.renderSvg(svgRoot, root, element, { offsetX, offsetY });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const opacity =
|
const opacity =
|
||||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
getTextEditor,
|
||||||
|
render,
|
||||||
|
updateTextEditor,
|
||||||
|
} from "./test-utils";
|
||||||
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
|
import React from "react";
|
||||||
|
import { expect, vi } from "vitest";
|
||||||
|
import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw";
|
||||||
|
|
||||||
|
vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => {
|
||||||
|
const module = (await importActual()) as any;
|
||||||
|
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...module,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const parseMermaidToExcalidrawSpy = vi.spyOn(
|
||||||
|
MermaidToExcalidraw,
|
||||||
|
"parseMermaidToExcalidraw",
|
||||||
|
);
|
||||||
|
|
||||||
|
parseMermaidToExcalidrawSpy.mockImplementation(
|
||||||
|
async (
|
||||||
|
definition: string,
|
||||||
|
options?: MermaidToExcalidraw.MermaidOptions | undefined,
|
||||||
|
) => {
|
||||||
|
const firstLine = definition.split("\n")[0];
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (firstLine === "flowchart TD") {
|
||||||
|
resolve({
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "Start",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: [],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 69.703125,
|
||||||
|
height: 44,
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
groupIds: [],
|
||||||
|
text: "Start",
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
link: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Stop",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: [],
|
||||||
|
x: 2.7109375,
|
||||||
|
y: 94,
|
||||||
|
width: 64.28125,
|
||||||
|
height: 44,
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
groupIds: [],
|
||||||
|
text: "Stop",
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
link: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Start_Stop",
|
||||||
|
type: "arrow",
|
||||||
|
groupIds: [],
|
||||||
|
x: 34.852,
|
||||||
|
y: 44,
|
||||||
|
strokeWidth: 2,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0, 50],
|
||||||
|
],
|
||||||
|
roundness: {
|
||||||
|
type: 2,
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
id: "Start",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
id: "Stop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error("ERROR"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.spyOn(React, "useRef").mockReturnValue({
|
||||||
|
current: {
|
||||||
|
parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test <MermaidToExcalidraw/>", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{
|
||||||
|
appState: {
|
||||||
|
activeTool: {
|
||||||
|
type: "mermaid",
|
||||||
|
lastActiveTool: null,
|
||||||
|
locked: false,
|
||||||
|
customType: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open mermaid popup when active tool is mermaid", async () => {
|
||||||
|
const dialog = document.querySelector(".dialog-mermaid")!;
|
||||||
|
|
||||||
|
expect(dialog.outerHTML).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close the popup and set the tool to selection when close button clicked", () => {
|
||||||
|
const dialog = document.querySelector(".dialog-mermaid")!;
|
||||||
|
const closeBtn = dialog.querySelector(".Dialog__close")!;
|
||||||
|
fireEvent.click(closeBtn);
|
||||||
|
expect(document.querySelector(".dialog-mermaid")).toBe(null);
|
||||||
|
expect(window.h.state.activeTool).toStrictEqual({
|
||||||
|
customType: null,
|
||||||
|
lastActiveTool: null,
|
||||||
|
locked: false,
|
||||||
|
type: "selection",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error in preview when mermaid library throws error", async () => {
|
||||||
|
const dialog = document.querySelector(".dialog-mermaid")!;
|
||||||
|
const selector = ".mermaid-to-excalidraw-wrapper-text textarea";
|
||||||
|
const editor = getTextEditor(selector);
|
||||||
|
|
||||||
|
expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull();
|
||||||
|
|
||||||
|
expect(editor.textContent).toMatchInlineSnapshot(`
|
||||||
|
"flowchart TD
|
||||||
|
A[Christmas] -->|Get money| B(Go shopping)
|
||||||
|
B --> C{Let me think}
|
||||||
|
C -->|One| D[Laptop]
|
||||||
|
C -->|Two| E[iPhone]
|
||||||
|
C -->|Three| F[test]"
|
||||||
|
`);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
updateTextEditor(editor, "flowchart TD1");
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getTextEditor(selector).textContent).toBe("flowchart TD1");
|
||||||
|
expect(dialog.querySelector('[data-testid="mermaid-error"]'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<div
|
||||||
|
data-testid="mermaid-error"
|
||||||
|
style="color: red; font-weight: 800; font-size: 30px; word-break: break-word; overflow: auto; max-height: 100%; text-align: center;"
|
||||||
|
>
|
||||||
|
Error!
|
||||||
|
<p
|
||||||
|
style="font-size: 18px; font-weight: 600;"
|
||||||
|
>
|
||||||
|
ERROR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = `
|
||||||
|
"<div class=\\"Modal Dialog dialog-mermaid\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-labelledby=\\"dialog-title\\" data-prevent-outside-click=\\"true\\"><div class=\\"Modal__background\\"></div><div class=\\"Modal__content\\" style=\\"--max-width: 800px;\\" tabindex=\\"0\\"><div class=\\"Island\\"><h2 id=\\"test-id-dialog-title\\" class=\\"Dialog__title\\"><span class=\\"Dialog__titleContent\\"><p style=\\"margin-bottom: 5px; margin-top: 2px;\\">Mermaid to Excalidraw</p><span style=\\"font-size: 15px; font-style: italic; font-weight: 500;\\">Currently only <a href=\\"https://mermaid.js.org/syntax/flowchart.html\\">flowcharts</a> are supported. The other types will be rendered as image in Excalidraw.<br></span></span></h2><button class=\\"Dialog__close\\" title=\\"Close\\" aria-label=\\"Close\\"><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g clip-path=\\"url(#a)\\" stroke=\\"currentColor\\" stroke-width=\\"1.25\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><path d=\\"M15 5 5 15M5 5l10 10\\"></path></g><defs><clipPath id=\\"a\\"><path fill=\\"#fff\\" d=\\"M0 0h20v20H0z\\"></path></clipPath></defs></svg></button><div class=\\"Dialog__content\\"><div class=\\"mermaid-to-excalidraw-wrapper\\"><div class=\\"mermaid-to-excalidraw-wrapper-text\\" style=\\"display: flex; flex-direction: column;\\"><label>Mermaid Syntax</label><textarea style=\\"padding: 0.85rem; border-radius: 8px; border: 1px solid #e4e4eb; white-space: pre-wrap;\\">flowchart TD
|
||||||
|
A[Christmas] -->|Get money| B(Go shopping)
|
||||||
|
B --> C{Let me think}
|
||||||
|
C -->|One| D[Laptop]
|
||||||
|
C -->|Two| E[iPhone]
|
||||||
|
C -->|Three| F[test]</textarea></div><div class=\\"mermaid-to-excalidraw-wrapper-preview\\" style=\\"display: flex; flex-direction: column;\\"><label>Preview</label><div class=\\"mermaid-to-excalidraw-wrapper-preview-canvas\\"><div></div></div><button type=\\"button\\" class=\\"excalidraw-button mermaid-to-excalidraw-wrapper-preview-insert\\">Insert<span style=\\"padding-left: 8px; display: flex;\\"><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\"><path d=\\"M4.16602 10H15.8327\\" stroke=\\"white\\" stroke-width=\\"1.25\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"></path><path d=\\"M12.5 13.3333L15.8333 10\\" stroke=\\"white\\" stroke-width=\\"1.25\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"></path><path d=\\"M12.5 6.66666L15.8333 9.99999\\" stroke=\\"white\\" stroke-width=\\"1.25\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"></path></svg></span></button></div></div></div></div></div></div>"
|
||||||
|
`;
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
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,16 +16,6 @@ import util from "util";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { getMimeType } from "../../data/blob";
|
import { getMimeType } from "../../data/blob";
|
||||||
import {
|
import {
|
||||||
SubtypeLoadedCb,
|
|
||||||
SubtypePrepFn,
|
|
||||||
SubtypeRecord,
|
|
||||||
checkRefreshOnSubtypeLoad,
|
|
||||||
prepareSubtype,
|
|
||||||
selectSubtype,
|
|
||||||
subtypeActionPredicate,
|
|
||||||
} from "../../element/subtypes";
|
|
||||||
import {
|
|
||||||
maybeGetSubtypeProps,
|
|
||||||
newEmbeddableElement,
|
newEmbeddableElement,
|
||||||
newFrameElement,
|
newFrameElement,
|
||||||
newFreeDrawElement,
|
newFreeDrawElement,
|
||||||
@@ -42,26 +32,6 @@ const readFile = util.promisify(fs.readFile);
|
|||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
export class API {
|
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[]) => {
|
static setSelectedElements = (elements: ExcalidrawElement[]) => {
|
||||||
h.setState({
|
h.setState({
|
||||||
selectedElementIds: elements.reduce((acc, element) => {
|
selectedElementIds: elements.reduce((acc, element) => {
|
||||||
@@ -142,8 +112,6 @@ export class API {
|
|||||||
verticalAlign?: T extends "text"
|
verticalAlign?: T extends "text"
|
||||||
? ExcalidrawTextElement["verticalAlign"]
|
? ExcalidrawTextElement["verticalAlign"]
|
||||||
: never;
|
: never;
|
||||||
subtype?: ExcalidrawElement["subtype"];
|
|
||||||
customData?: ExcalidrawElement["customData"];
|
|
||||||
boundElements?: ExcalidrawGenericElement["boundElements"];
|
boundElements?: ExcalidrawGenericElement["boundElements"];
|
||||||
containerId?: T extends "text"
|
containerId?: T extends "text"
|
||||||
? ExcalidrawTextElement["containerId"]
|
? ExcalidrawTextElement["containerId"]
|
||||||
@@ -172,14 +140,6 @@ export class API {
|
|||||||
|
|
||||||
const appState = h?.state || getDefaultAppState();
|
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<
|
const base: Omit<
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
| "id"
|
| "id"
|
||||||
@@ -195,7 +155,6 @@ export class API {
|
|||||||
| "link"
|
| "link"
|
||||||
| "updated"
|
| "updated"
|
||||||
> = {
|
> = {
|
||||||
...custom,
|
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
angle: rest.angle ?? 0,
|
angle: rest.angle ?? 0,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"toolBar": {
|
|
||||||
"test": "Test",
|
|
||||||
"test2": "Test 2",
|
|
||||||
"test3": "Test 3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -274,7 +274,7 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
@@ -417,7 +417,7 @@ describe("Test Linear Elements", () => {
|
|||||||
lastSegmentMidpoint[1] + delta,
|
lastSegmentMidpoint[1] + delta,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
@@ -520,7 +520,7 @@ describe("Test Linear Elements", () => {
|
|||||||
// delete 3rd point
|
// delete 3rd point
|
||||||
deletePoint(points[2]);
|
deletePoint(points[2]);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(20);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
|
|
||||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
@@ -567,7 +567,7 @@ describe("Test Linear Elements", () => {
|
|||||||
lastSegmentMidpoint[0] + delta,
|
lastSegmentMidpoint[0] + delta,
|
||||||
lastSegmentMidpoint[1] + delta,
|
lastSegmentMidpoint[1] + delta,
|
||||||
]);
|
]);
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
|
|||||||
@@ -1,689 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -265,3 +265,15 @@ expect.extend({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateTextEditor = (
|
||||||
|
editor: HTMLTextAreaElement,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
fireEvent.change(editor, { target: { value } });
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTextEditor = (selector: string) => {
|
||||||
|
return document.querySelector(selector) as HTMLTextAreaElement;
|
||||||
|
};
|
||||||
|
|||||||
+9
-18
@@ -18,7 +18,6 @@ import {
|
|||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
ExcalidrawEmbeddableElement,
|
ExcalidrawEmbeddableElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { Action } from "./actions/types";
|
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
import { LinearElementEditor } from "./element/linearElementEditor";
|
import { LinearElementEditor } from "./element/linearElementEditor";
|
||||||
@@ -32,12 +31,6 @@ import { ClipboardData } from "./clipboard";
|
|||||||
import { isOverScrollBars } from "./scene";
|
import { isOverScrollBars } from "./scene";
|
||||||
import { MaybeTransformHandleType } from "./element/transformHandles";
|
import { MaybeTransformHandleType } from "./element/transformHandles";
|
||||||
import Library from "./data/library";
|
import Library from "./data/library";
|
||||||
import {
|
|
||||||
SubtypeMethods,
|
|
||||||
Subtype,
|
|
||||||
SubtypePrepFn,
|
|
||||||
SubtypeRecord,
|
|
||||||
} from "./element/subtypes";
|
|
||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { ContextMenuItems } from "./components/ContextMenu";
|
import { ContextMenuItems } from "./components/ContextMenu";
|
||||||
@@ -99,7 +92,8 @@ export type LastActiveTool =
|
|||||||
| "eraser"
|
| "eraser"
|
||||||
| "hand"
|
| "hand"
|
||||||
| "frame"
|
| "frame"
|
||||||
| "embeddable";
|
| "embeddable"
|
||||||
|
| "mermaid";
|
||||||
customType: null;
|
customType: null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -193,10 +187,6 @@ export type AppState = {
|
|||||||
// (e.g. text element when typing into the input)
|
// (e.g. text element when typing into the input)
|
||||||
editingElement: NonDeletedExcalidrawElement | null;
|
editingElement: NonDeletedExcalidrawElement | null;
|
||||||
editingLinearElement: LinearElementEditor | null;
|
editingLinearElement: LinearElementEditor | null;
|
||||||
activeSubtypes?: Subtype[];
|
|
||||||
customData?: {
|
|
||||||
[subtype: Subtype]: ExcalidrawElement["customData"];
|
|
||||||
};
|
|
||||||
activeTool: {
|
activeTool: {
|
||||||
/**
|
/**
|
||||||
* indicates a previous tool we should revert back to if we deselect the
|
* indicates a previous tool we should revert back to if we deselect the
|
||||||
@@ -211,7 +201,8 @@ export type AppState = {
|
|||||||
| "eraser"
|
| "eraser"
|
||||||
| "hand"
|
| "hand"
|
||||||
| "frame"
|
| "frame"
|
||||||
| "embeddable";
|
| "embeddable"
|
||||||
|
| "mermaid";
|
||||||
customType: null;
|
customType: null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -538,6 +529,11 @@ export type AppClassProperties = {
|
|||||||
onInsertElements: App["onInsertElements"];
|
onInsertElements: App["onInsertElements"];
|
||||||
onExportImage: App["onExportImage"];
|
onExportImage: App["onExportImage"];
|
||||||
lastViewportPosition: App["lastViewportPosition"];
|
lastViewportPosition: App["lastViewportPosition"];
|
||||||
|
scrollToContent: App["scrollToContent"];
|
||||||
|
addFiles: App["addFiles"];
|
||||||
|
setSelection: App["setSelection"];
|
||||||
|
addElementsFromPasteOrLibrary: App["addElementsFromPasteOrLibrary"];
|
||||||
|
setActiveTool: App["setActiveTool"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
@@ -620,11 +616,6 @@ export type ExcalidrawImperativeAPI = {
|
|||||||
getSceneElements: InstanceType<typeof App>["getSceneElements"];
|
getSceneElements: InstanceType<typeof App>["getSceneElements"];
|
||||||
getAppState: () => InstanceType<typeof App>["state"];
|
getAppState: () => InstanceType<typeof App>["state"];
|
||||||
getFiles: () => InstanceType<typeof App>["files"];
|
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"];
|
refresh: InstanceType<typeof App>["refresh"];
|
||||||
setToast: InstanceType<typeof App>["setToast"];
|
setToast: InstanceType<typeof App>["setToast"];
|
||||||
addFiles: (data: BinaryFileData[]) => void;
|
addFiles: (data: BinaryFileData[]) => void;
|
||||||
|
|||||||
+2
-1
@@ -376,7 +376,8 @@ export const updateActiveTool = (
|
|||||||
| "eraser"
|
| "eraser"
|
||||||
| "hand"
|
| "hand"
|
||||||
| "frame"
|
| "frame"
|
||||||
| "embeddable";
|
| "embeddable"
|
||||||
|
| "mermaid";
|
||||||
}
|
}
|
||||||
| { type: "custom"; customType: string }
|
| { type: "custom"; customType: string }
|
||||||
) & { lastActiveToolBeforeEraser?: LastActiveTool },
|
) & { lastActiveToolBeforeEraser?: LastActiveTool },
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
coverage: {
|
coverage: {
|
||||||
reporter: ["text", "json-summary", "json"],
|
reporter: ["text", "json-summary", "json", "html"],
|
||||||
lines: 70,
|
lines: 70,
|
||||||
branches: 70,
|
branches: 70,
|
||||||
functions: 68,
|
functions: 68,
|
||||||
|
|||||||
Reference in New Issue
Block a user