Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
671ed94d74 | ||
|
|
d5ac76d4ea | ||
|
|
2b19d53549 | ||
|
|
b4abfad638 | ||
|
|
a39640ead1 | ||
|
|
84bd9bd4ff |
@@ -44,6 +44,7 @@
|
|||||||
"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",
|
||||||
|
"pngjs": "7.0.0",
|
||||||
"points-on-curve": "0.2.0",
|
"points-on-curve": "0.2.0",
|
||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"@types/lodash.throttle": "4.1.7",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.3",
|
||||||
"@types/pica": "5.1.3",
|
"@types/pica": "5.1.3",
|
||||||
|
"@types/pngjs": "6.0.1",
|
||||||
"@types/react": "18.0.15",
|
"@types/react": "18.0.15",
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/resize-observer-browser": "0.1.7",
|
"@types/resize-observer-browser": "0.1.7",
|
||||||
|
|||||||
+15
-8
@@ -825,6 +825,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (typeof this.props.name !== "undefined") {
|
if (typeof this.props.name !== "undefined") {
|
||||||
name = this.props.name;
|
name = this.props.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editingElement =
|
||||||
|
editingElement || actionResult.appState?.editingElement || null;
|
||||||
|
|
||||||
|
if (editingElement?.isDeleted) {
|
||||||
|
editingElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
(state) => {
|
(state) => {
|
||||||
// using Object.assign instead of spread to fool TS 4.2.2+ into
|
// using Object.assign instead of spread to fool TS 4.2.2+ into
|
||||||
@@ -835,8 +843,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// or programmatically from the host, so it will need to be
|
// or programmatically from the host, so it will need to be
|
||||||
// rewritten later
|
// rewritten later
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
editingElement:
|
editingElement,
|
||||||
editingElement || actionResult.appState?.editingElement || null,
|
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
gridSize,
|
gridSize,
|
||||||
@@ -1347,6 +1354,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// failsafe in case the state is being updated in incorrect order resulting
|
||||||
|
// in the editingElement being now a deleted element
|
||||||
|
if (this.state.editingElement?.isDeleted) {
|
||||||
|
this.setState({ editingElement: null });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.selectedLinearElement &&
|
this.state.selectedLinearElement &&
|
||||||
!this.state.selectedElementIds[this.state.selectedLinearElement.elementId]
|
!this.state.selectedElementIds[this.state.selectedLinearElement.elementId]
|
||||||
@@ -4142,12 +4155,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (pointerDownState.resize.handleType) {
|
if (pointerDownState.resize.handleType) {
|
||||||
setCursor(
|
|
||||||
this.canvas,
|
|
||||||
getCursorForResizingElement({
|
|
||||||
transformHandleType: pointerDownState.resize.handleType,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
pointerDownState.resize.isResizing = true;
|
pointerDownState.resize.isResizing = true;
|
||||||
pointerDownState.resize.offset = tupleToCoors(
|
pointerDownState.resize.offset = tupleToCoors(
|
||||||
getResizeOffsetXY(
|
getResizeOffsetXY(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isTransparent, isWritableElement } from "../../utils";
|
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
import { TopPicks } from "./TopPicks";
|
import { TopPicks } from "./TopPicks";
|
||||||
@@ -121,11 +121,14 @@ const ColorPickerPopupContent = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onCloseAutoFocus={(e) => {
|
onCloseAutoFocus={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
// prevents focusing the trigger
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
// return focus to excalidraw container
|
// return focus to excalidraw container unless
|
||||||
if (container) {
|
// user focuses an interactive element, such as a button, or
|
||||||
|
// enters the text editor by clicking on canvas with the text tool
|
||||||
|
if (container && !isInteractive(document.activeElement)) {
|
||||||
container.focus();
|
container.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+82
-3
@@ -13,6 +13,7 @@ import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
|||||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
||||||
import { restore, restoreLibraryItems } from "./restore";
|
import { restore, restoreLibraryItems } from "./restore";
|
||||||
import { ImportedLibraryData } from "./types";
|
import { ImportedLibraryData } from "./types";
|
||||||
|
import { PNG } from "pngjs/browser";
|
||||||
|
|
||||||
const parseFileContents = async (blob: Blob | File) => {
|
const parseFileContents = async (blob: Blob | File) => {
|
||||||
let contents: string;
|
let contents: string;
|
||||||
@@ -210,9 +211,7 @@ export const loadLibraryFromBlob = async (
|
|||||||
return parseLibraryJSON(await parseFileContents(blob), defaultStatus);
|
return parseLibraryJSON(await parseFileContents(blob), defaultStatus);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canvasToBlob = async (
|
const _canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
|
||||||
canvas: HTMLCanvasElement,
|
|
||||||
): Promise<Blob> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
@@ -232,6 +231,86 @@ export const canvasToBlob = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const canvasToBlob = async (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
): Promise<Blob> => {
|
||||||
|
const tileWidth = 1000;
|
||||||
|
const tileHeight = 1000;
|
||||||
|
const tileDataArray: Uint8ClampedArray[][] = []; // Two-dimensional array to store tile data
|
||||||
|
|
||||||
|
const { width: canvasWidth, height: canvasHeight } = canvas;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("No canvas context");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to process each tile
|
||||||
|
function processTile(tileX: number, tileY: number) {
|
||||||
|
// Calculate the starting and ending coordinates for the tile
|
||||||
|
const startX = tileX * tileWidth;
|
||||||
|
const startY = tileY * tileHeight;
|
||||||
|
const endX = Math.min(startX + tileWidth, canvasWidth);
|
||||||
|
const endY = Math.min(startY + tileHeight, canvasHeight);
|
||||||
|
|
||||||
|
// Get the image data for the tile directly from the main canvas
|
||||||
|
const imageData = ctx!.getImageData(
|
||||||
|
startX,
|
||||||
|
startY,
|
||||||
|
endX - startX,
|
||||||
|
endY - startY,
|
||||||
|
).data;
|
||||||
|
|
||||||
|
// Store the tile data in the two-dimensional array
|
||||||
|
tileDataArray[tileY] = tileDataArray[tileY] || [];
|
||||||
|
tileDataArray[tileY][tileX] = imageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.time("tiling");
|
||||||
|
// Iterate over the tiles and process each one
|
||||||
|
for (let tileY = 0; tileY < canvasHeight / tileHeight; tileY++) {
|
||||||
|
for (let tileX = 0; tileX < canvasWidth / tileWidth; tileX++) {
|
||||||
|
processTile(tileX, tileY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.timeEnd("tiling");
|
||||||
|
|
||||||
|
console.time("create png");
|
||||||
|
// Create a new PNG image with the final dimensions
|
||||||
|
const finalImage = new PNG({ width: canvasWidth, height: canvasHeight });
|
||||||
|
console.timeEnd("create png");
|
||||||
|
|
||||||
|
console.time("concat tiles");
|
||||||
|
// Merge the tiles into the final image
|
||||||
|
for (let tileY = 0; tileY < canvasHeight / tileHeight; tileY++) {
|
||||||
|
for (let tileX = 0; tileX < canvasWidth / tileWidth; tileX++) {
|
||||||
|
const imageData = tileDataArray[tileY][tileX];
|
||||||
|
const destX = tileX * tileWidth;
|
||||||
|
const destY = tileY * tileHeight;
|
||||||
|
|
||||||
|
// Copy the pixels from the tile to the final image
|
||||||
|
for (let y = 0; y < tileHeight; y++) {
|
||||||
|
for (let x = 0; x < tileWidth; x++) {
|
||||||
|
const index = (y * tileWidth + x) * 4;
|
||||||
|
const destIndex = ((destY + y) * canvasWidth + destX + x) * 4;
|
||||||
|
finalImage.data[destIndex] = imageData[index];
|
||||||
|
finalImage.data[destIndex + 1] = imageData[index + 1];
|
||||||
|
finalImage.data[destIndex + 2] = imageData[index + 2];
|
||||||
|
finalImage.data[destIndex + 3] = imageData[index + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.timeEnd("concat tiles");
|
||||||
|
|
||||||
|
console.time("create buffer");
|
||||||
|
const buffer = PNG.sync.write(finalImage);
|
||||||
|
console.timeEnd("create buffer");
|
||||||
|
|
||||||
|
return new Blob([buffer], { type: "image/png" });
|
||||||
|
};
|
||||||
|
|
||||||
/** generates SHA-1 digest from supplied file (if not supported, falls back
|
/** generates SHA-1 digest from supplied file (if not supported, falls back
|
||||||
to a 40-char base64 random id) */
|
to a 40-char base64 random id) */
|
||||||
export const generateIdFromFile = async (file: File): Promise<FileId> => {
|
export const generateIdFromFile = async (file: File): Promise<FileId> => {
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ export const exportCanvas = async (
|
|||||||
document.body.appendChild(tempCanvas);
|
document.body.appendChild(tempCanvas);
|
||||||
|
|
||||||
if (type === "png") {
|
if (type === "png") {
|
||||||
|
console.time("export png");
|
||||||
let blob = await canvasToBlob(tempCanvas);
|
let blob = await canvasToBlob(tempCanvas);
|
||||||
|
console.timeEnd("export png");
|
||||||
tempCanvas.remove();
|
tempCanvas.remove();
|
||||||
if (appState.exportEmbedScene) {
|
if (appState.exportEmbedScene) {
|
||||||
blob = await (
|
blob = await (
|
||||||
|
|||||||
@@ -890,26 +890,34 @@ const rotateMultipleElements = (
|
|||||||
centerY,
|
centerY,
|
||||||
centerAngle + origAngle - element.angle,
|
centerAngle + origAngle - element.angle,
|
||||||
);
|
);
|
||||||
mutateElement(element, {
|
|
||||||
x: element.x + (rotatedCX - cx),
|
mutateElement(
|
||||||
y: element.y + (rotatedCY - cy),
|
element,
|
||||||
angle: normalizeAngle(centerAngle + origAngle),
|
{
|
||||||
});
|
x: element.x + (rotatedCX - cx),
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
y: element.y + (rotatedCY - cy),
|
||||||
if (boundTextElementId) {
|
angle: normalizeAngle(centerAngle + origAngle),
|
||||||
const textElement =
|
},
|
||||||
Scene.getScene(element)?.getElement<ExcalidrawTextElementWithContainer>(
|
false,
|
||||||
boundTextElementId,
|
);
|
||||||
);
|
|
||||||
if (textElement && !isArrowElement(element)) {
|
updateBoundElements(element, { simultaneouslyUpdated: elements });
|
||||||
mutateElement(textElement, {
|
|
||||||
x: textElement.x + (rotatedCX - cx),
|
const boundText = getBoundTextElement(element);
|
||||||
y: textElement.y + (rotatedCY - cy),
|
if (boundText && !isArrowElement(element)) {
|
||||||
|
mutateElement(
|
||||||
|
boundText,
|
||||||
|
{
|
||||||
|
x: boundText.x + (rotatedCX - cx),
|
||||||
|
y: boundText.y + (rotatedCY - cy),
|
||||||
angle: normalizeAngle(centerAngle + origAngle),
|
angle: normalizeAngle(centerAngle + origAngle),
|
||||||
});
|
},
|
||||||
}
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Scene.getScene(elements[0])?.informMutation();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getResizeOffsetXY = (
|
export const getResizeOffsetXY = (
|
||||||
|
|||||||
@@ -49,7 +49,18 @@ describe("Test wrapText", () => {
|
|||||||
{
|
{
|
||||||
desc: "break all characters when width of each character is less than container width",
|
desc: "break all characters when width of each character is less than container width",
|
||||||
width: 25,
|
width: 25,
|
||||||
res: `H\ne\nl\nl\no \nw\nh\na\nt\ns \nu\np`,
|
res: `H
|
||||||
|
e
|
||||||
|
l
|
||||||
|
l
|
||||||
|
o
|
||||||
|
w
|
||||||
|
h
|
||||||
|
a
|
||||||
|
t
|
||||||
|
s
|
||||||
|
u
|
||||||
|
p`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
@@ -79,7 +90,8 @@ up`,
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("When text contain new lines", () => {
|
describe("When text contain new lines", () => {
|
||||||
const text = "Hello\nwhats up";
|
const text = `Hello
|
||||||
|
whats up`;
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
@@ -89,7 +101,18 @@ up`,
|
|||||||
{
|
{
|
||||||
desc: "break all characters when width of each character is less than container width",
|
desc: "break all characters when width of each character is less than container width",
|
||||||
width: 25,
|
width: 25,
|
||||||
res: `H\ne\nl\nl\no\nw\nh\na\nt\ns \nu\np`,
|
res: `H
|
||||||
|
e
|
||||||
|
l
|
||||||
|
l
|
||||||
|
o
|
||||||
|
w
|
||||||
|
h
|
||||||
|
a
|
||||||
|
t
|
||||||
|
s
|
||||||
|
u
|
||||||
|
p`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
@@ -126,7 +149,13 @@ whats up`,
|
|||||||
desc: "fit characters of long string as per container width and break words as per the width",
|
desc: "fit characters of long string as per container width and break words as per the width",
|
||||||
|
|
||||||
width: 130,
|
width: 130,
|
||||||
res: `hellolongte\nxtthisiswha\ntsupwithyou\nIamtypinggg\nggandtyping\ngg break it \nnow`,
|
res: `hellolongte
|
||||||
|
xtthisiswha
|
||||||
|
tsupwithyou
|
||||||
|
Iamtypinggg
|
||||||
|
ggandtyping
|
||||||
|
gg break it
|
||||||
|
now`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
||||||
@@ -161,7 +190,7 @@ whats up`,
|
|||||||
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
|
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
|
||||||
const res = wrapText(text, font, 110);
|
const res = wrapText(text, font, 110);
|
||||||
expect(res).toBe(
|
expect(res).toBe(
|
||||||
`Wikipedia \nis hosted \nby \nWikimedia- \nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts \na range-of \nother \nprojects`,
|
`Wikipedia \nis hosted \nby \nWikimedia-\nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts\na range-of\nother \nprojects`,
|
||||||
);
|
);
|
||||||
|
|
||||||
text = "Hello thereusing-now";
|
text = "Hello thereusing-now";
|
||||||
|
|||||||
+17
-42
@@ -76,7 +76,6 @@ export const redrawTextBoundingBox = (
|
|||||||
boundTextUpdates.text,
|
boundTextUpdates.text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
textElement.lineHeight,
|
textElement.lineHeight,
|
||||||
maxWidth,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
boundTextUpdates.width = metrics.width;
|
boundTextUpdates.width = metrics.width;
|
||||||
@@ -196,7 +195,6 @@ export const handleBindTextResize = (
|
|||||||
text,
|
text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
textElement.lineHeight,
|
textElement.lineHeight,
|
||||||
maxWidth,
|
|
||||||
);
|
);
|
||||||
nextHeight = metrics.height;
|
nextHeight = metrics.height;
|
||||||
nextWidth = metrics.width;
|
nextWidth = metrics.width;
|
||||||
@@ -285,7 +283,6 @@ export const measureText = (
|
|||||||
text: string,
|
text: string,
|
||||||
font: FontString,
|
font: FontString,
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
maxWidth?: number | null,
|
|
||||||
) => {
|
) => {
|
||||||
text = text
|
text = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -295,14 +292,7 @@ export const measureText = (
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
const fontSize = parseFloat(font);
|
const fontSize = parseFloat(font);
|
||||||
const height = getTextHeight(text, fontSize, lineHeight);
|
const height = getTextHeight(text, fontSize, lineHeight);
|
||||||
let width = getTextWidth(text, font);
|
const width = getTextWidth(text, font);
|
||||||
// Since we now preserve trailing whitespaces so if the text has
|
|
||||||
// trailing whitespaces, it will be considered in the width and thus width
|
|
||||||
// computed might be much higher than the allowed max width
|
|
||||||
// by the container hence making sure the width never goes beyond the max width.
|
|
||||||
if (maxWidth) {
|
|
||||||
width = Math.min(width, maxWidth);
|
|
||||||
}
|
|
||||||
const baseline = measureBaseline(text, font, lineHeight);
|
const baseline = measureBaseline(text, font, lineHeight);
|
||||||
return { width, height, baseline };
|
return { width, height, baseline };
|
||||||
};
|
};
|
||||||
@@ -390,7 +380,7 @@ export const getApproxMinLineHeight = (
|
|||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
export const getLineWidth = (text: string, font: FontString) => {
|
const getLineWidth = (text: string, font: FontString) => {
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
canvas = document.createElement("canvas");
|
canvas = document.createElement("canvas");
|
||||||
}
|
}
|
||||||
@@ -450,8 +440,10 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
|
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
|
const spaceWidth = getLineWidth(" ", font);
|
||||||
|
|
||||||
let currentLine = "";
|
let currentLine = "";
|
||||||
let currentLineWidthTillNow = 0;
|
let currentLineWidthTillNow = 0;
|
||||||
@@ -467,7 +459,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
};
|
};
|
||||||
originalLines.forEach((originalLine) => {
|
originalLines.forEach((originalLine) => {
|
||||||
const currentLineWidth = getLineWidth(originalLine, font);
|
const currentLineWidth = getTextWidth(originalLine, font);
|
||||||
|
|
||||||
// Push the line if its <= maxWidth
|
// Push the line if its <= maxWidth
|
||||||
if (currentLineWidth <= maxWidth) {
|
if (currentLineWidth <= maxWidth) {
|
||||||
@@ -515,25 +507,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// push current line if appending space exceeds max width
|
// push current line if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
resetParams();
|
resetParams();
|
||||||
// space needs to be appended before next word
|
// space needs to be appended before next word
|
||||||
// as currentLine contains chars which couldn't be appended
|
// as currentLine contains chars which couldn't be appended
|
||||||
// to previous line unless the line ends with hyphen to sync
|
// to previous line unless the line ends with hyphen to sync
|
||||||
// with css word-wrap
|
// with css word-wrap
|
||||||
} else if (!currentLine.endsWith("-") && index < words.length) {
|
} else if (!currentLine.endsWith("-")) {
|
||||||
currentLine += " ";
|
currentLine += " ";
|
||||||
|
currentLineWidthTillNow += spaceWidth;
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
} else {
|
} else {
|
||||||
// Start appending words in a line till max width reached
|
// Start appending words in a line till max width reached
|
||||||
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
||||||
const word = words[index];
|
const word = words[index];
|
||||||
currentLineWidthTillNow = getLineWidth(
|
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||||
`${currentLine + word}`.trimEnd(),
|
|
||||||
font,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentLineWidthTillNow > maxWidth) {
|
if (currentLineWidthTillNow > maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
@@ -541,20 +531,24 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
index++;
|
||||||
|
|
||||||
// if word ends with "-" then we don't need to add space
|
// if word ends with "-" then we don't need to add space
|
||||||
// to sync with css word-wrap
|
// to sync with css word-wrap
|
||||||
const shouldAppendSpace = !word.endsWith("-");
|
const shouldAppendSpace = !word.endsWith("-");
|
||||||
currentLine += word;
|
currentLine += word;
|
||||||
|
|
||||||
if (shouldAppendSpace && index < words.length) {
|
if (shouldAppendSpace) {
|
||||||
currentLine += " ";
|
currentLine += " ";
|
||||||
}
|
}
|
||||||
index++;
|
|
||||||
|
|
||||||
// Push the word if appending space exceeds max width
|
// Push the word if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
lines.push(currentLine);
|
if (shouldAppendSpace) {
|
||||||
|
lines.push(currentLine.slice(0, -1));
|
||||||
|
} else {
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
resetParams();
|
resetParams();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -977,22 +971,3 @@ export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
|
|||||||
}
|
}
|
||||||
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSpacesOffsetForLine = (
|
|
||||||
element: ExcalidrawTextElement,
|
|
||||||
line: string,
|
|
||||||
font: FontString,
|
|
||||||
) => {
|
|
||||||
const container = getContainerElement(element);
|
|
||||||
const trailingSpacesWidth =
|
|
||||||
getLineWidth(line, font) - getLineWidth(line.trimEnd(), font);
|
|
||||||
const maxWidth = container ? getBoundTextMaxWidth(container) : element.width;
|
|
||||||
const availableWidth = maxWidth - getLineWidth(line.trimEnd(), font);
|
|
||||||
let spacesOffset = 0;
|
|
||||||
if (element.textAlign === TEXT_ALIGN.CENTER) {
|
|
||||||
spacesOffset = -Math.min(trailingSpacesWidth / 2, availableWidth / 2);
|
|
||||||
} else if (element.textAlign === TEXT_ALIGN.RIGHT) {
|
|
||||||
spacesOffset = -Math.min(availableWidth, trailingSpacesWidth);
|
|
||||||
}
|
|
||||||
return spacesOffset;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
|||||||
const tab = " ";
|
const tab = " ";
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
const getTextEditor = () => {
|
||||||
|
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", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@@ -190,9 +201,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -214,9 +223,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -243,9 +250,7 @@ describe("textWysiwyg", () => {
|
|||||||
textElement = UI.createElement("text");
|
textElement = UI.createElement("text");
|
||||||
|
|
||||||
mouse.clickOn(textElement);
|
mouse.clickOn(textElement);
|
||||||
textarea = document.querySelector(
|
textarea = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
)!;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -455,17 +460,11 @@ describe("textWysiwyg", () => {
|
|||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(750, 300);
|
mouse.clickAt(750, 300);
|
||||||
|
|
||||||
textarea = document.querySelector(
|
textarea = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(
|
||||||
)!;
|
textarea,
|
||||||
fireEvent.change(textarea, {
|
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||||
target: {
|
);
|
||||||
value:
|
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
textarea.dispatchEvent(new Event("input"));
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
textarea.blur();
|
textarea.blur();
|
||||||
expect(textarea.style.width).toBe("792px");
|
expect(textarea.style.width).toBe("792px");
|
||||||
@@ -513,11 +512,9 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -543,11 +540,9 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
expect(text.angle).toBe(rectangle.angle);
|
expect(text.angle).toBe(rectangle.angle);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -572,9 +567,7 @@ describe("textWysiwyg", () => {
|
|||||||
API.setSelectedElements([diamond]);
|
API.setSelectedElements([diamond]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
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");
|
||||||
@@ -587,9 +580,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(diamond.height).toBe(50020);
|
expect(diamond.height).toBe(50020);
|
||||||
|
|
||||||
// Clearing text to simulate height decrease
|
// Clearing text to simulate height decrease
|
||||||
expect(() =>
|
expect(() => updateTextEditor(editor, "")).not.toThrow();
|
||||||
fireEvent.input(editor, { target: { value: "" } }),
|
|
||||||
).not.toThrow();
|
|
||||||
|
|
||||||
expect(diamond.height).toBe(70);
|
expect(diamond.height).toBe(70);
|
||||||
});
|
});
|
||||||
@@ -611,9 +602,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 = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -628,11 +617,9 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -652,13 +639,11 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -689,11 +674,8 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello World!");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -717,17 +699,9 @@ describe("textWysiwyg", () => {
|
|||||||
freedraw.y + freedraw.height / 2,
|
freedraw.y + freedraw.height / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello World!");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: {
|
|
||||||
value: "Hello World!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
|
|
||||||
expect(freedraw.boundElements).toBe(null);
|
expect(freedraw.boundElements).toBe(null);
|
||||||
expect(h.elements[1].type).toBe("text");
|
expect(h.elements[1].type).toBe("text");
|
||||||
@@ -759,11 +733,9 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -776,17 +748,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
updateTextEditor(
|
||||||
target: {
|
editor,
|
||||||
value: "Excalidraw is an opensource virtual collaborative whiteboard",
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
expect(h.elements[1].type).toBe("text");
|
expect(h.elements[1].type).toBe("text");
|
||||||
@@ -826,12 +793,10 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.down();
|
mouse.down();
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
@@ -841,9 +806,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
@@ -876,17 +839,9 @@ 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 = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
updateTextEditor(editor, "Hello World!");
|
||||||
target: {
|
|
||||||
value: "Hello World!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -905,17 +860,8 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: {
|
|
||||||
value: "Hello",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -943,13 +889,11 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -982,11 +926,9 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -1005,11 +947,9 @@ 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 = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
// should center align horizontally and vertically by default
|
// should center align horizontally and vertically by default
|
||||||
@@ -1024,9 +964,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -1049,9 +987,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -1089,11 +1025,9 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1106,11 +1040,9 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
expect(rectangle.width).toBe(90);
|
expect(rectangle.width).toBe(90);
|
||||||
@@ -1128,11 +1060,9 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
@@ -1162,11 +1092,9 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("undo should work", async () => {
|
it("undo should work", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: h.elements[1].id, type: "text" },
|
{ id: h.elements[1].id, type: "text" },
|
||||||
@@ -1201,12 +1129,10 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: " " } });
|
updateTextEditor(editor, " ");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([]);
|
expect(rectangle.boundElements).toStrictEqual([]);
|
||||||
expect(h.elements[1].isDeleted).toBe(true);
|
expect(h.elements[1].isDeleted).toBe(true);
|
||||||
@@ -1225,9 +1151,9 @@ describe("textWysiwyg", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
text: "Online whiteboard collaboration made easy",
|
text: "Online whiteboard collaboration made easy",
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [container, text];
|
h.elements = [container, text];
|
||||||
API.setSelectedElements([container, text]);
|
API.setSelectedElements([container, text]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
@@ -1258,11 +1184,9 @@ 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 = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
@@ -1272,9 +1196,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1287,12 +1209,8 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello World!");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
@@ -1316,12 +1234,8 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello World!");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
@@ -1352,17 +1266,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
|
||||||
editor.blur();
|
editor.blur();
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
editor.select();
|
editor.select();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1473,17 +1382,12 @@ 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 = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
updateTextEditor(
|
||||||
target: {
|
editor,
|
||||||
value: "Excalidraw is an opensource virtual collaborative whiteboard",
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, TEXT_ALIGN, isSafari } from "../constants";
|
import { CLASSES, isSafari } from "../constants";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
measureText,
|
getTextWidth,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
wrapText,
|
||||||
@@ -196,8 +196,6 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
maxWidth = getBoundTextMaxWidth(container);
|
maxWidth = getBoundTextMaxWidth(container);
|
||||||
textElementWidth = Math.min(textElementWidth, maxWidth);
|
|
||||||
|
|
||||||
maxHeight = getBoundTextMaxHeight(
|
maxHeight = getBoundTextMaxHeight(
|
||||||
container,
|
container,
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
@@ -232,16 +230,7 @@ export const textWysiwyg = ({
|
|||||||
coordY = y;
|
coordY = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let spacesOffset = 0;
|
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||||
if (updatedTextElement.textAlign === TEXT_ALIGN.CENTER) {
|
|
||||||
spacesOffset = Math.max(0, updatedTextElement.width / 2 - maxWidth / 2);
|
|
||||||
} else if (updatedTextElement.textAlign === TEXT_ALIGN.RIGHT) {
|
|
||||||
spacesOffset = Math.max(0, updatedTextElement.width - maxWidth);
|
|
||||||
}
|
|
||||||
const [viewportX, viewportY] = getViewportCoords(
|
|
||||||
coordX + spacesOffset,
|
|
||||||
coordY,
|
|
||||||
);
|
|
||||||
const initialSelectionStart = editable.selectionStart;
|
const initialSelectionStart = editable.selectionStart;
|
||||||
const initialSelectionEnd = editable.selectionEnd;
|
const initialSelectionEnd = editable.selectionEnd;
|
||||||
const initialLength = editable.value.length;
|
const initialLength = editable.value.length;
|
||||||
@@ -373,12 +362,7 @@ export const textWysiwyg = ({
|
|||||||
font,
|
font,
|
||||||
getBoundTextMaxWidth(container),
|
getBoundTextMaxWidth(container),
|
||||||
);
|
);
|
||||||
const { width } = measureText(
|
const width = getTextWidth(wrappedText, font);
|
||||||
wrappedText,
|
|
||||||
font,
|
|
||||||
element.lineHeight,
|
|
||||||
getBoundTextMaxWidth(container),
|
|
||||||
);
|
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+4
@@ -120,3 +120,7 @@ declare module "image-blob-reduce" {
|
|||||||
const reduce: ImageBlobReduce.ImageBlobReduceStatic;
|
const reduce: ImageBlobReduce.ImageBlobReduceStatic;
|
||||||
export = reduce;
|
export = reduce;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "pngjs/browser" {
|
||||||
|
export { PNG } from "pngjs";
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ import {
|
|||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getSpacesOffsetForLine,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
@@ -320,13 +319,13 @@ const drawElementOnCanvas = (
|
|||||||
}
|
}
|
||||||
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
||||||
context.save();
|
context.save();
|
||||||
const font = getFontString(element);
|
context.font = getFontString(element);
|
||||||
context.font = font;
|
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle = element.strokeColor;
|
||||||
context.textAlign = element.textAlign as CanvasTextAlign;
|
context.textAlign = element.textAlign as CanvasTextAlign;
|
||||||
|
|
||||||
// Canvas does not support multiline text by default
|
// Canvas does not support multiline text by default
|
||||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
|
|
||||||
const horizontalOffset =
|
const horizontalOffset =
|
||||||
element.textAlign === "center"
|
element.textAlign === "center"
|
||||||
? element.width / 2
|
? element.width / 2
|
||||||
@@ -337,17 +336,11 @@ const drawElementOnCanvas = (
|
|||||||
element.fontSize,
|
element.fontSize,
|
||||||
element.lineHeight,
|
element.lineHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
const verticalOffset = element.height - element.baseline;
|
const verticalOffset = element.height - element.baseline;
|
||||||
for (let index = 0; index < lines.length; index++) {
|
for (let index = 0; index < lines.length; index++) {
|
||||||
const spacesOffset = getSpacesOffsetForLine(
|
|
||||||
element,
|
|
||||||
lines[index],
|
|
||||||
font,
|
|
||||||
);
|
|
||||||
context.fillText(
|
context.fillText(
|
||||||
lines[index].trimEnd(),
|
lines[index],
|
||||||
horizontalOffset + spacesOffset,
|
horizontalOffset,
|
||||||
(index + 1) * lineHeightPx - verticalOffset,
|
(index + 1) * lineHeightPx - verticalOffset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1151,7 +1151,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(
|
expect(
|
||||||
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
"Online whiteboard collaboration
|
"Online whiteboard collaboration
|
||||||
made easy"
|
made easy"
|
||||||
`);
|
`);
|
||||||
const handleBindTextResizeSpy = jest.spyOn(
|
const handleBindTextResizeSpy = jest.spyOn(
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ export const isInputLike = (
|
|||||||
target instanceof HTMLTextAreaElement ||
|
target instanceof HTMLTextAreaElement ||
|
||||||
target instanceof HTMLSelectElement;
|
target instanceof HTMLSelectElement;
|
||||||
|
|
||||||
|
export const isInteractive = (target: Element | EventTarget | null) => {
|
||||||
|
return (
|
||||||
|
isInputLike(target) ||
|
||||||
|
(target instanceof Element && !!target.closest("label, button"))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isWritableElement = (
|
export const isWritableElement = (
|
||||||
target: Element | EventTarget | null,
|
target: Element | EventTarget | null,
|
||||||
): target is
|
): target is
|
||||||
|
|||||||
@@ -2714,6 +2714,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/pica/-/pica-5.1.3.tgz#5ef64529a1f83f7d6586a8bf75a8a00be32aca02"
|
resolved "https://registry.yarnpkg.com/@types/pica/-/pica-5.1.3.tgz#5ef64529a1f83f7d6586a8bf75a8a00be32aca02"
|
||||||
integrity sha512-13SEyETRE5psd9bE0AmN+0M1tannde2fwHfLVaVIljkbL9V0OfFvKwCicyeDvVYLkmjQWEydbAlsDsmjrdyTOg==
|
integrity sha512-13SEyETRE5psd9bE0AmN+0M1tannde2fwHfLVaVIljkbL9V0OfFvKwCicyeDvVYLkmjQWEydbAlsDsmjrdyTOg==
|
||||||
|
|
||||||
|
"@types/pngjs@6.0.1":
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.1.tgz#c711ec3fbbf077fed274ecccaf85dd4673130072"
|
||||||
|
integrity sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/prettier@^2.1.5":
|
"@types/prettier@^2.1.5":
|
||||||
version "2.7.2"
|
version "2.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0"
|
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0"
|
||||||
@@ -8227,6 +8234,11 @@ png-chunks-extract@1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
crc-32 "^0.3.0"
|
crc-32 "^0.3.0"
|
||||||
|
|
||||||
|
pngjs@7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26"
|
||||||
|
integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==
|
||||||
|
|
||||||
points-on-curve@0.2.0, points-on-curve@^0.2.0:
|
points-on-curve@0.2.0, points-on-curve@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/points-on-curve/-/points-on-curve-0.2.0.tgz#7dbb98c43791859434284761330fa893cb81b4d1"
|
resolved "https://registry.yarnpkg.com/points-on-curve/-/points-on-curve-0.2.0.tgz#7dbb98c43791859434284761330fa893cb81b4d1"
|
||||||
|
|||||||
Reference in New Issue
Block a user