Merge branch 'master' into zsviczian-loop-lock
# Conflicts: # packages/excalidraw/actions/actionProperties.tsx # packages/excalidraw/components/RadioSelection.tsx
This commit is contained in:
commit
d645e0ed13
@ -52,7 +52,7 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw .panelColumn {
|
.excalidraw .selected-shape-actions {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"default": "./dist/prod/index.js"
|
"default": "./dist/prod/index.js"
|
||||||
},
|
},
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./../common/dist/types/common/src/*.d.ts"
|
"types": "./dist/types/common/src/*.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -10,6 +10,7 @@ export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|||||||
export const isWindows = /^Win/.test(navigator.platform);
|
export const isWindows = /^Win/.test(navigator.platform);
|
||||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||||
export const isFirefox =
|
export const isFirefox =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
"netscape" in window &&
|
"netscape" in window &&
|
||||||
navigator.userAgent.indexOf("rv:") > 1 &&
|
navigator.userAgent.indexOf("rv:") > 1 &&
|
||||||
navigator.userAgent.indexOf("Gecko") > 1;
|
navigator.userAgent.indexOf("Gecko") > 1;
|
||||||
@ -255,7 +256,7 @@ export const EXPORT_DATA_TYPES = {
|
|||||||
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
|
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_SOURCE =
|
export const getExportSource = () =>
|
||||||
window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
|
window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
|
||||||
|
|
||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
|
@ -46,7 +46,7 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
unitsPerEm: 1000,
|
unitsPerEm: 1000,
|
||||||
ascender: 1011,
|
ascender: 1011,
|
||||||
descender: -353,
|
descender: -353,
|
||||||
lineHeight: 1.35,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[FONT_FAMILY["Lilita One"]]: {
|
[FONT_FAMILY["Lilita One"]]: {
|
||||||
@ -116,7 +116,7 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
unitsPerEm: 1000,
|
unitsPerEm: 1000,
|
||||||
ascender: 880,
|
ascender: 880,
|
||||||
descender: -144,
|
descender: -144,
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
fallback: true,
|
fallback: true,
|
||||||
},
|
},
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"default": "./dist/prod/index.js"
|
"default": "./dist/prod/index.js"
|
||||||
},
|
},
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./../element/dist/types/element/src/*.d.ts"
|
"types": "./dist/types/element/src/*.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -33,6 +33,8 @@ const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/;
|
|||||||
const RE_GH_GIST_EMBED =
|
const RE_GH_GIST_EMBED =
|
||||||
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i;
|
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i;
|
||||||
|
|
||||||
|
const RE_MSFORMS = /^(?:https?:\/\/)?forms\.microsoft\.com\//;
|
||||||
|
|
||||||
// not anchored to start to allow <blockquote> twitter embeds
|
// not anchored to start to allow <blockquote> twitter embeds
|
||||||
const RE_TWITTER =
|
const RE_TWITTER =
|
||||||
/(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/;
|
/(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/;
|
||||||
@ -69,6 +71,7 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"val.town",
|
"val.town",
|
||||||
"giphy.com",
|
"giphy.com",
|
||||||
"reddit.com",
|
"reddit.com",
|
||||||
|
"forms.microsoft.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ALLOW_SAME_ORIGIN = new Set([
|
const ALLOW_SAME_ORIGIN = new Set([
|
||||||
@ -82,6 +85,7 @@ const ALLOW_SAME_ORIGIN = new Set([
|
|||||||
"*.simplepdf.eu",
|
"*.simplepdf.eu",
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"reddit.com",
|
"reddit.com",
|
||||||
|
"forms.microsoft.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const createSrcDoc = (body: string) => {
|
export const createSrcDoc = (body: string) => {
|
||||||
@ -206,6 +210,10 @@ export const getEmbedLink = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RE_MSFORMS.test(link) && !link.includes("embed=true")) {
|
||||||
|
link += link.includes("?") ? "&embed=true" : "?embed=true";
|
||||||
|
}
|
||||||
|
|
||||||
if (RE_TWITTER.test(link)) {
|
if (RE_TWITTER.test(link)) {
|
||||||
const postId = link.match(RE_TWITTER)![1];
|
const postId = link.match(RE_TWITTER)![1];
|
||||||
// the embed srcdoc still supports twitter.com domain only.
|
// the embed srcdoc still supports twitter.com domain only.
|
||||||
|
@ -351,12 +351,20 @@ const generateElementCanvas = (
|
|||||||
|
|
||||||
export const DEFAULT_LINK_SIZE = 14;
|
export const DEFAULT_LINK_SIZE = 14;
|
||||||
|
|
||||||
const IMAGE_PLACEHOLDER_IMG = document.createElement("img");
|
const IMAGE_PLACEHOLDER_IMG =
|
||||||
|
typeof document !== "undefined"
|
||||||
|
? document.createElement("img")
|
||||||
|
: ({ src: "" } as HTMLImageElement); // mock image element outside of browser
|
||||||
|
|
||||||
IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||||
`<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="image" class="svg-inline--fa fa-image fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#888" d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"></path></svg>`,
|
`<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="image" class="svg-inline--fa fa-image fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#888" d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"></path></svg>`,
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
const IMAGE_ERROR_PLACEHOLDER_IMG = document.createElement("img");
|
const IMAGE_ERROR_PLACEHOLDER_IMG =
|
||||||
|
typeof document !== "undefined"
|
||||||
|
? document.createElement("img")
|
||||||
|
: ({ src: "" } as HTMLImageElement); // mock image element outside of browser
|
||||||
|
|
||||||
IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||||
`<svg viewBox="0 0 668 668" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48ZM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56ZM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.81709 0 0 .81709 124.825 145.825)"/><path d="M256 8C119.034 8 8 119.033 8 256c0 136.967 111.034 248 248 248s248-111.034 248-248S392.967 8 256 8Zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676ZM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.30366 0 0 .30366 506.822 60.065)"/></svg>`,
|
`<svg viewBox="0 0 668 668" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48ZM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56ZM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.81709 0 0 .81709 124.825 145.825)"/><path d="M256 8C119.034 8 8 119.033 8 256c0 136.967 111.034 248 248 248s248-111.034 248-248S392.967 8 256 8Zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676ZM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.30366 0 0 .30366 506.822 60.065)"/></svg>`,
|
||||||
)}`;
|
)}`;
|
||||||
|
@ -78,7 +78,7 @@ import type { Scene } from "@excalidraw/element";
|
|||||||
import type { CaptureUpdateActionType } from "@excalidraw/element";
|
import type { CaptureUpdateActionType } from "@excalidraw/element";
|
||||||
|
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { RadioSelection } from "../components/RadioSelection";
|
||||||
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
||||||
import { FontPicker } from "../components/FontPicker/FontPicker";
|
import { FontPicker } from "../components/FontPicker/FontPicker";
|
||||||
import { IconPicker } from "../components/IconPicker";
|
import { IconPicker } from "../components/IconPicker";
|
||||||
@ -457,50 +457,52 @@ export const actionChangeFillStyle = register({
|
|||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fill")}</legend>
|
<legend>{t("labels.fill")}</legend>
|
||||||
<ButtonIconSelect
|
<div className="buttonList">
|
||||||
type="button"
|
<RadioSelection
|
||||||
options={[
|
type="button"
|
||||||
{
|
options={[
|
||||||
value: "hachure",
|
{
|
||||||
text: `${
|
value: "hachure",
|
||||||
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
|
text: `${
|
||||||
} (${getShortcutKey("Alt-Click")})`,
|
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
|
||||||
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
} (${getShortcutKey("Alt-Click")})`,
|
||||||
active: allElementsZigZag ? true : undefined,
|
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||||
testId: `fill-hachure`,
|
active: allElementsZigZag ? true : undefined,
|
||||||
},
|
testId: `fill-hachure`,
|
||||||
{
|
},
|
||||||
value: "cross-hatch",
|
{
|
||||||
text: t("labels.crossHatch"),
|
value: "cross-hatch",
|
||||||
icon: FillCrossHatchIcon,
|
text: t("labels.crossHatch"),
|
||||||
testId: `fill-cross-hatch`,
|
icon: FillCrossHatchIcon,
|
||||||
},
|
testId: `fill-cross-hatch`,
|
||||||
{
|
},
|
||||||
value: "solid",
|
{
|
||||||
text: t("labels.solid"),
|
value: "solid",
|
||||||
icon: FillSolidIcon,
|
text: t("labels.solid"),
|
||||||
testId: `fill-solid`,
|
icon: FillSolidIcon,
|
||||||
},
|
testId: `fill-solid`,
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) => element.fillStyle,
|
app,
|
||||||
(element) => element.hasOwnProperty("fillStyle"),
|
(element) => element.fillStyle,
|
||||||
(hasSelection) =>
|
(element) => element.hasOwnProperty("fillStyle"),
|
||||||
hasSelection ? null : appState.currentItemFillStyle,
|
(hasSelection) =>
|
||||||
)}
|
hasSelection ? null : appState.currentItemFillStyle,
|
||||||
onClick={(value, event) => {
|
)}
|
||||||
const nextValue =
|
onClick={(value, event) => {
|
||||||
event.altKey &&
|
const nextValue =
|
||||||
value === "hachure" &&
|
event.altKey &&
|
||||||
selectedElements.every((el) => el.fillStyle === "hachure")
|
value === "hachure" &&
|
||||||
? "zigzag"
|
selectedElements.every((el) => el.fillStyle === "hachure")
|
||||||
: value;
|
? "zigzag"
|
||||||
|
: value;
|
||||||
|
|
||||||
updateData(nextValue);
|
updateData(nextValue);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -524,38 +526,40 @@ export const actionChangeStrokeWidth = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.strokeWidth")}</legend>
|
<legend>{t("labels.strokeWidth")}</legend>
|
||||||
<ButtonIconSelect
|
<div className="buttonList">
|
||||||
group="stroke-width"
|
<RadioSelection
|
||||||
options={[
|
group="stroke-width"
|
||||||
{
|
options={[
|
||||||
value: STROKE_WIDTH.thin,
|
{
|
||||||
text: t("labels.thin"),
|
value: STROKE_WIDTH.thin,
|
||||||
icon: StrokeWidthBaseIcon,
|
text: t("labels.thin"),
|
||||||
testId: "strokeWidth-thin",
|
icon: StrokeWidthBaseIcon,
|
||||||
},
|
testId: "strokeWidth-thin",
|
||||||
{
|
},
|
||||||
value: STROKE_WIDTH.bold,
|
{
|
||||||
text: t("labels.bold"),
|
value: STROKE_WIDTH.bold,
|
||||||
icon: StrokeWidthBoldIcon,
|
text: t("labels.bold"),
|
||||||
testId: "strokeWidth-bold",
|
icon: StrokeWidthBoldIcon,
|
||||||
},
|
testId: "strokeWidth-bold",
|
||||||
{
|
},
|
||||||
value: STROKE_WIDTH.extraBold,
|
{
|
||||||
text: t("labels.extraBold"),
|
value: STROKE_WIDTH.extraBold,
|
||||||
icon: StrokeWidthExtraBoldIcon,
|
text: t("labels.extraBold"),
|
||||||
testId: "strokeWidth-extraBold",
|
icon: StrokeWidthExtraBoldIcon,
|
||||||
},
|
testId: "strokeWidth-extraBold",
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) => element.strokeWidth,
|
app,
|
||||||
(element) => element.hasOwnProperty("strokeWidth"),
|
(element) => element.strokeWidth,
|
||||||
(hasSelection) =>
|
(element) => element.hasOwnProperty("strokeWidth"),
|
||||||
hasSelection ? null : appState.currentItemStrokeWidth,
|
(hasSelection) =>
|
||||||
)}
|
hasSelection ? null : appState.currentItemStrokeWidth,
|
||||||
onChange={(value) => updateData(value)}
|
)}
|
||||||
/>
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -579,35 +583,37 @@ export const actionChangeSloppiness = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.sloppiness")}</legend>
|
<legend>{t("labels.sloppiness")}</legend>
|
||||||
<ButtonIconSelect
|
<div className="buttonList">
|
||||||
group="sloppiness"
|
<RadioSelection
|
||||||
options={[
|
group="sloppiness"
|
||||||
{
|
options={[
|
||||||
value: 0,
|
{
|
||||||
text: t("labels.architect"),
|
value: 0,
|
||||||
icon: SloppinessArchitectIcon,
|
text: t("labels.architect"),
|
||||||
},
|
icon: SloppinessArchitectIcon,
|
||||||
{
|
},
|
||||||
value: 1,
|
{
|
||||||
text: t("labels.artist"),
|
value: 1,
|
||||||
icon: SloppinessArtistIcon,
|
text: t("labels.artist"),
|
||||||
},
|
icon: SloppinessArtistIcon,
|
||||||
{
|
},
|
||||||
value: 2,
|
{
|
||||||
text: t("labels.cartoonist"),
|
value: 2,
|
||||||
icon: SloppinessCartoonistIcon,
|
text: t("labels.cartoonist"),
|
||||||
},
|
icon: SloppinessCartoonistIcon,
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) => element.roughness,
|
app,
|
||||||
(element) => element.hasOwnProperty("roughness"),
|
(element) => element.roughness,
|
||||||
(hasSelection) =>
|
(element) => element.hasOwnProperty("roughness"),
|
||||||
hasSelection ? null : appState.currentItemRoughness,
|
(hasSelection) =>
|
||||||
)}
|
hasSelection ? null : appState.currentItemRoughness,
|
||||||
onChange={(value) => updateData(value)}
|
)}
|
||||||
/>
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -630,35 +636,37 @@ export const actionChangeStrokeStyle = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.strokeStyle")}</legend>
|
<legend>{t("labels.strokeStyle")}</legend>
|
||||||
<ButtonIconSelect
|
<div className="buttonList">
|
||||||
group="strokeStyle"
|
<RadioSelection
|
||||||
options={[
|
group="strokeStyle"
|
||||||
{
|
options={[
|
||||||
value: "solid",
|
{
|
||||||
text: t("labels.strokeStyle_solid"),
|
value: "solid",
|
||||||
icon: StrokeWidthBaseIcon,
|
text: t("labels.strokeStyle_solid"),
|
||||||
},
|
icon: StrokeWidthBaseIcon,
|
||||||
{
|
},
|
||||||
value: "dashed",
|
{
|
||||||
text: t("labels.strokeStyle_dashed"),
|
value: "dashed",
|
||||||
icon: StrokeStyleDashedIcon,
|
text: t("labels.strokeStyle_dashed"),
|
||||||
},
|
icon: StrokeStyleDashedIcon,
|
||||||
{
|
},
|
||||||
value: "dotted",
|
{
|
||||||
text: t("labels.strokeStyle_dotted"),
|
value: "dotted",
|
||||||
icon: StrokeStyleDottedIcon,
|
text: t("labels.strokeStyle_dotted"),
|
||||||
},
|
icon: StrokeStyleDottedIcon,
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) => element.strokeStyle,
|
app,
|
||||||
(element) => element.hasOwnProperty("strokeStyle"),
|
(element) => element.strokeStyle,
|
||||||
(hasSelection) =>
|
(element) => element.hasOwnProperty("strokeStyle"),
|
||||||
hasSelection ? null : appState.currentItemStrokeStyle,
|
(hasSelection) =>
|
||||||
)}
|
hasSelection ? null : appState.currentItemStrokeStyle,
|
||||||
onChange={(value) => updateData(value)}
|
)}
|
||||||
/>
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -697,63 +705,65 @@ export const actionChangeFontSize = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontSize")}</legend>
|
<legend>{t("labels.fontSize")}</legend>
|
||||||
<ButtonIconSelect
|
<div className="buttonList">
|
||||||
group="font-size"
|
<RadioSelection
|
||||||
options={[
|
group="font-size"
|
||||||
{
|
options={[
|
||||||
value: 16,
|
{
|
||||||
text: t("labels.small"),
|
value: 16,
|
||||||
icon: FontSizeSmallIcon,
|
text: t("labels.small"),
|
||||||
testId: "fontSize-small",
|
icon: FontSizeSmallIcon,
|
||||||
},
|
testId: "fontSize-small",
|
||||||
{
|
},
|
||||||
value: 20,
|
{
|
||||||
text: t("labels.medium"),
|
value: 20,
|
||||||
icon: FontSizeMediumIcon,
|
text: t("labels.medium"),
|
||||||
testId: "fontSize-medium",
|
icon: FontSizeMediumIcon,
|
||||||
},
|
testId: "fontSize-medium",
|
||||||
{
|
},
|
||||||
value: 28,
|
{
|
||||||
text: t("labels.large"),
|
value: 28,
|
||||||
icon: FontSizeLargeIcon,
|
text: t("labels.large"),
|
||||||
testId: "fontSize-large",
|
icon: FontSizeLargeIcon,
|
||||||
},
|
testId: "fontSize-large",
|
||||||
{
|
},
|
||||||
value: 36,
|
{
|
||||||
text: t("labels.veryLarge"),
|
value: 36,
|
||||||
icon: FontSizeExtraLargeIcon,
|
text: t("labels.veryLarge"),
|
||||||
testId: "fontSize-veryLarge",
|
icon: FontSizeExtraLargeIcon,
|
||||||
},
|
testId: "fontSize-veryLarge",
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) => {
|
app,
|
||||||
if (isTextElement(element)) {
|
(element) => {
|
||||||
return element.fontSize;
|
if (isTextElement(element)) {
|
||||||
}
|
return element.fontSize;
|
||||||
const boundTextElement = getBoundTextElement(
|
}
|
||||||
element,
|
const boundTextElement = getBoundTextElement(
|
||||||
app.scene.getNonDeletedElementsMap(),
|
element,
|
||||||
);
|
app.scene.getNonDeletedElementsMap(),
|
||||||
if (boundTextElement) {
|
);
|
||||||
return boundTextElement.fontSize;
|
if (boundTextElement) {
|
||||||
}
|
return boundTextElement.fontSize;
|
||||||
return null;
|
}
|
||||||
},
|
return null;
|
||||||
(element) =>
|
},
|
||||||
isTextElement(element) ||
|
(element) =>
|
||||||
getBoundTextElement(
|
isTextElement(element) ||
|
||||||
element,
|
getBoundTextElement(
|
||||||
app.scene.getNonDeletedElementsMap(),
|
element,
|
||||||
) !== null,
|
app.scene.getNonDeletedElementsMap(),
|
||||||
(hasSelection) =>
|
) !== null,
|
||||||
hasSelection
|
(hasSelection) =>
|
||||||
? null
|
hasSelection
|
||||||
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
? null
|
||||||
)}
|
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||||
onChange={(value) => updateData(value)}
|
)}
|
||||||
/>
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -1225,52 +1235,54 @@ export const actionChangeTextAlign = register({
|
|||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.textAlign")}</legend>
|
<legend>{t("labels.textAlign")}</legend>
|
||||||
<ButtonIconSelect<TextAlign | false>
|
<div className="buttonList">
|
||||||
group="text-align"
|
<RadioSelection<TextAlign | false>
|
||||||
options={[
|
group="text-align"
|
||||||
{
|
options={[
|
||||||
value: "left",
|
{
|
||||||
text: t("labels.left"),
|
value: "left",
|
||||||
icon: TextAlignLeftIcon,
|
text: t("labels.left"),
|
||||||
testId: "align-left",
|
icon: TextAlignLeftIcon,
|
||||||
},
|
testId: "align-left",
|
||||||
{
|
},
|
||||||
value: "center",
|
{
|
||||||
text: t("labels.center"),
|
value: "center",
|
||||||
icon: TextAlignCenterIcon,
|
text: t("labels.center"),
|
||||||
testId: "align-horizontal-center",
|
icon: TextAlignCenterIcon,
|
||||||
},
|
testId: "align-horizontal-center",
|
||||||
{
|
},
|
||||||
value: "right",
|
{
|
||||||
text: t("labels.right"),
|
value: "right",
|
||||||
icon: TextAlignRightIcon,
|
text: t("labels.right"),
|
||||||
testId: "align-right",
|
icon: TextAlignRightIcon,
|
||||||
},
|
testId: "align-right",
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) => {
|
app,
|
||||||
if (isTextElement(element)) {
|
(element) => {
|
||||||
return element.textAlign;
|
if (isTextElement(element)) {
|
||||||
}
|
return element.textAlign;
|
||||||
const boundTextElement = getBoundTextElement(
|
}
|
||||||
element,
|
const boundTextElement = getBoundTextElement(
|
||||||
elementsMap,
|
element,
|
||||||
);
|
elementsMap,
|
||||||
if (boundTextElement) {
|
);
|
||||||
return boundTextElement.textAlign;
|
if (boundTextElement) {
|
||||||
}
|
return boundTextElement.textAlign;
|
||||||
return null;
|
}
|
||||||
},
|
return null;
|
||||||
(element) =>
|
},
|
||||||
isTextElement(element) ||
|
(element) =>
|
||||||
getBoundTextElement(element, elementsMap) !== null,
|
isTextElement(element) ||
|
||||||
(hasSelection) =>
|
getBoundTextElement(element, elementsMap) !== null,
|
||||||
hasSelection ? null : appState.currentItemTextAlign,
|
(hasSelection) =>
|
||||||
)}
|
hasSelection ? null : appState.currentItemTextAlign,
|
||||||
onChange={(value) => updateData(value)}
|
)}
|
||||||
/>
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -1313,54 +1325,56 @@ export const actionChangeVerticalAlign = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<ButtonIconSelect<VerticalAlign | false>
|
<div className="buttonList">
|
||||||
group="text-align"
|
<RadioSelection<VerticalAlign | false>
|
||||||
options={[
|
group="text-align"
|
||||||
{
|
options={[
|
||||||
value: VERTICAL_ALIGN.TOP,
|
{
|
||||||
text: t("labels.alignTop"),
|
value: VERTICAL_ALIGN.TOP,
|
||||||
icon: <TextAlignTopIcon theme={appState.theme} />,
|
text: t("labels.alignTop"),
|
||||||
testId: "align-top",
|
icon: <TextAlignTopIcon theme={appState.theme} />,
|
||||||
},
|
testId: "align-top",
|
||||||
{
|
},
|
||||||
value: VERTICAL_ALIGN.MIDDLE,
|
{
|
||||||
text: t("labels.centerVertically"),
|
value: VERTICAL_ALIGN.MIDDLE,
|
||||||
icon: <TextAlignMiddleIcon theme={appState.theme} />,
|
text: t("labels.centerVertically"),
|
||||||
testId: "align-middle",
|
icon: <TextAlignMiddleIcon theme={appState.theme} />,
|
||||||
},
|
testId: "align-middle",
|
||||||
{
|
},
|
||||||
value: VERTICAL_ALIGN.BOTTOM,
|
{
|
||||||
text: t("labels.alignBottom"),
|
value: VERTICAL_ALIGN.BOTTOM,
|
||||||
icon: <TextAlignBottomIcon theme={appState.theme} />,
|
text: t("labels.alignBottom"),
|
||||||
testId: "align-bottom",
|
icon: <TextAlignBottomIcon theme={appState.theme} />,
|
||||||
},
|
testId: "align-bottom",
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) => {
|
app,
|
||||||
if (isTextElement(element) && element.containerId) {
|
(element) => {
|
||||||
return element.verticalAlign;
|
if (isTextElement(element) && element.containerId) {
|
||||||
}
|
return element.verticalAlign;
|
||||||
const boundTextElement = getBoundTextElement(
|
}
|
||||||
element,
|
const boundTextElement = getBoundTextElement(
|
||||||
app.scene.getNonDeletedElementsMap(),
|
element,
|
||||||
);
|
app.scene.getNonDeletedElementsMap(),
|
||||||
if (boundTextElement) {
|
);
|
||||||
return boundTextElement.verticalAlign;
|
if (boundTextElement) {
|
||||||
}
|
return boundTextElement.verticalAlign;
|
||||||
return null;
|
}
|
||||||
},
|
return null;
|
||||||
(element) =>
|
},
|
||||||
isTextElement(element) ||
|
(element) =>
|
||||||
getBoundTextElement(
|
isTextElement(element) ||
|
||||||
element,
|
getBoundTextElement(
|
||||||
app.scene.getNonDeletedElementsMap(),
|
element,
|
||||||
) !== null,
|
app.scene.getNonDeletedElementsMap(),
|
||||||
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
) !== null,
|
||||||
)}
|
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||||
onChange={(value) => updateData(value)}
|
)}
|
||||||
/>
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -1408,34 +1422,39 @@ export const actionChangeRoundness = register({
|
|||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.edges")}</legend>
|
<legend>{t("labels.edges")}</legend>
|
||||||
<ButtonIconSelect
|
<div className="buttonList">
|
||||||
group="edges"
|
<RadioSelection
|
||||||
options={[
|
group="edges"
|
||||||
{
|
options={[
|
||||||
value: "sharp",
|
{
|
||||||
text: t("labels.sharp"),
|
value: "sharp",
|
||||||
icon: EdgeSharpIcon,
|
text: t("labels.sharp"),
|
||||||
},
|
icon: EdgeSharpIcon,
|
||||||
{
|
},
|
||||||
value: "round",
|
{
|
||||||
text: t("labels.round"),
|
value: "round",
|
||||||
icon: EdgeRoundIcon,
|
text: t("labels.round"),
|
||||||
},
|
icon: EdgeRoundIcon,
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) =>
|
app,
|
||||||
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
(element) =>
|
||||||
(element) =>
|
hasLegacyRoundness
|
||||||
!isArrowElement(element) && element.hasOwnProperty("roundness"),
|
? null
|
||||||
(hasSelection) =>
|
: element.roundness
|
||||||
hasSelection ? null : appState.currentItemRoundness,
|
? "round"
|
||||||
)}
|
: "sharp",
|
||||||
onChange={(value) => updateData(value)}
|
(element) =>
|
||||||
>
|
!isArrowElement(element) && element.hasOwnProperty("roundness"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemRoundness,
|
||||||
|
)}
|
||||||
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
{renderAction("togglePolygon")}
|
{renderAction("togglePolygon")}
|
||||||
</ButtonIconSelect>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -1798,48 +1817,50 @@ export const actionChangeArrowType = register({
|
|||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.arrowtypes")}</legend>
|
<legend>{t("labels.arrowtypes")}</legend>
|
||||||
<ButtonIconSelect
|
<div className="buttonList">
|
||||||
group="arrowtypes"
|
<RadioSelection
|
||||||
options={[
|
group="arrowtypes"
|
||||||
{
|
options={[
|
||||||
value: ARROW_TYPE.sharp,
|
{
|
||||||
text: t("labels.arrowtype_sharp"),
|
value: ARROW_TYPE.sharp,
|
||||||
icon: sharpArrowIcon,
|
text: t("labels.arrowtype_sharp"),
|
||||||
testId: "sharp-arrow",
|
icon: sharpArrowIcon,
|
||||||
},
|
testId: "sharp-arrow",
|
||||||
{
|
},
|
||||||
value: ARROW_TYPE.round,
|
{
|
||||||
text: t("labels.arrowtype_round"),
|
value: ARROW_TYPE.round,
|
||||||
icon: roundArrowIcon,
|
text: t("labels.arrowtype_round"),
|
||||||
testId: "round-arrow",
|
icon: roundArrowIcon,
|
||||||
},
|
testId: "round-arrow",
|
||||||
{
|
},
|
||||||
value: ARROW_TYPE.elbow,
|
{
|
||||||
text: t("labels.arrowtype_elbowed"),
|
value: ARROW_TYPE.elbow,
|
||||||
icon: elbowArrowIcon,
|
text: t("labels.arrowtype_elbowed"),
|
||||||
testId: "elbow-arrow",
|
icon: elbowArrowIcon,
|
||||||
},
|
testId: "elbow-arrow",
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
app,
|
elements,
|
||||||
(element) => {
|
app,
|
||||||
if (isArrowElement(element)) {
|
(element) => {
|
||||||
return element.elbowed
|
if (isArrowElement(element)) {
|
||||||
? ARROW_TYPE.elbow
|
return element.elbowed
|
||||||
: element.roundness
|
? ARROW_TYPE.elbow
|
||||||
? ARROW_TYPE.round
|
: element.roundness
|
||||||
: ARROW_TYPE.sharp;
|
? ARROW_TYPE.round
|
||||||
}
|
: ARROW_TYPE.sharp;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) => isArrowElement(element),
|
(element) => isArrowElement(element),
|
||||||
(hasSelection) =>
|
(hasSelection) =>
|
||||||
hasSelection ? null : appState.currentItemArrowType,
|
hasSelection ? null : appState.currentItemArrowType,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -154,7 +154,7 @@ export const SelectedShapeActions = ({
|
|||||||
!isSingleElementBoundContainer && alignActionsPredicate(appState, app);
|
!isSingleElementBoundContainer && alignActionsPredicate(appState, app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="selected-shape-actions">
|
||||||
<div>
|
<div>
|
||||||
{canChangeStrokeColor(appState, targetElements) &&
|
{canChangeStrokeColor(appState, targetElements) &&
|
||||||
renderAction("changeStrokeColor")}
|
renderAction("changeStrokeColor")}
|
||||||
|
@ -7276,8 +7276,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
// If we click on something
|
// If we click on something
|
||||||
} else if (hitElement != null) {
|
} else if (hitElement != null) {
|
||||||
|
// == deep selection ==
|
||||||
// on CMD/CTRL, drill down to hit element regardless of groups etc.
|
// on CMD/CTRL, drill down to hit element regardless of groups etc.
|
||||||
if (event[KEYS.CTRL_OR_CMD]) {
|
if (event[KEYS.CTRL_OR_CMD]) {
|
||||||
|
if (event.altKey) {
|
||||||
|
// ctrl + alt means we're lasso selecting
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!this.state.selectedElementIds[hitElement.id]) {
|
if (!this.state.selectedElementIds[hitElement.id]) {
|
||||||
pointerDownState.hit.wasAddedToSelection = true;
|
pointerDownState.hit.wasAddedToSelection = true;
|
||||||
}
|
}
|
||||||
@ -8636,17 +8641,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
if (event.altKey) {
|
if (event.altKey) {
|
||||||
this.setActiveTool(
|
flushSync(() => {
|
||||||
{ type: "lasso", fromSelection: true },
|
this.setActiveTool(
|
||||||
event.shiftKey,
|
{ type: "lasso", fromSelection: true },
|
||||||
);
|
event.shiftKey,
|
||||||
this.lassoTrail.startPath(
|
);
|
||||||
pointerDownState.origin.x,
|
this.lassoTrail.startPath(
|
||||||
pointerDownState.origin.y,
|
pointerDownState.origin.x,
|
||||||
event.shiftKey,
|
pointerDownState.origin.y,
|
||||||
);
|
event.shiftKey,
|
||||||
this.setAppState({
|
);
|
||||||
selectionElement: null,
|
this.setAppState({
|
||||||
|
selectionElement: null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||||
@ -9521,7 +9528,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// if we're editing a line, pointerup shouldn't switch selection if
|
// if we're editing a line, pointerup shouldn't switch selection if
|
||||||
// box selected
|
// box selected
|
||||||
(!this.state.editingLinearElement ||
|
(!this.state.editingLinearElement ||
|
||||||
!pointerDownState.boxSelection.hasOccurred)
|
!pointerDownState.boxSelection.hasOccurred) &&
|
||||||
|
// hitElement can be set when alt + ctrl to toggle lasso and we will
|
||||||
|
// just respect the selected elements from lasso instead
|
||||||
|
this.state.activeTool.type !== "lasso"
|
||||||
) {
|
) {
|
||||||
// when inside line editor, shift selects points instead
|
// when inside line editor, shift selects points instead
|
||||||
if (childEvent.shiftKey && !this.state.editingLinearElement) {
|
if (childEvent.shiftKey && !this.state.editingLinearElement) {
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
export const ButtonSelect = <T extends Object>({
|
|
||||||
options,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
group,
|
|
||||||
}: {
|
|
||||||
options: { value: T; text: string }[];
|
|
||||||
value: T | null;
|
|
||||||
onChange: (value: T) => void;
|
|
||||||
group: string;
|
|
||||||
}) => (
|
|
||||||
<div className="buttonList">
|
|
||||||
{options.map((option) => (
|
|
||||||
<label
|
|
||||||
key={option.text}
|
|
||||||
className={clsx({ active: value === option.value })}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={group}
|
|
||||||
onChange={() => onChange(option.value)}
|
|
||||||
checked={value === option.value}
|
|
||||||
/>
|
|
||||||
{option.text}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
@ -6,7 +6,7 @@ import { FONT_FAMILY } from "@excalidraw/common";
|
|||||||
import type { FontFamilyValues } from "@excalidraw/element/types";
|
import type { FontFamilyValues } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { ButtonIconSelect } from "../ButtonIconSelect";
|
import { RadioSelection } from "../RadioSelection";
|
||||||
import { ButtonSeparator } from "../ButtonSeparator";
|
import { ButtonSeparator } from "../ButtonSeparator";
|
||||||
import {
|
import {
|
||||||
FontFamilyCodeIcon,
|
FontFamilyCodeIcon,
|
||||||
@ -82,12 +82,14 @@ export const FontPicker = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="dialog" aria-modal="true" className="FontPicker__container">
|
<div role="dialog" aria-modal="true" className="FontPicker__container">
|
||||||
<ButtonIconSelect<FontFamilyValues | false>
|
<div className="buttonList">
|
||||||
type="button"
|
<RadioSelection<FontFamilyValues | false>
|
||||||
options={defaultFonts}
|
type="button"
|
||||||
value={selectedFontFamily}
|
options={defaultFonts}
|
||||||
onClick={onSelectCallback}
|
value={selectedFontFamily}
|
||||||
/>
|
onClick={onSelectCallback}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ButtonSeparator />
|
<ButtonSeparator />
|
||||||
<Popover.Root open={isOpened} onOpenChange={onPopupChange}>
|
<Popover.Root open={isOpened} onOpenChange={onPopupChange}>
|
||||||
<FontPickerTrigger selectedFontFamily={selectedFontFamily} />
|
<FontPickerTrigger selectedFontFamily={selectedFontFamily} />
|
||||||
|
@ -5,10 +5,10 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
EDITOR_LS_KEYS,
|
EDITOR_LS_KEYS,
|
||||||
EXPORT_DATA_TYPES,
|
EXPORT_DATA_TYPES,
|
||||||
EXPORT_SOURCE,
|
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
VERSIONS,
|
VERSIONS,
|
||||||
chunk,
|
chunk,
|
||||||
|
getExportSource,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
||||||
@ -281,7 +281,7 @@ const PublishLibrary = ({
|
|||||||
const libContent: ExportedLibraryData = {
|
const libContent: ExportedLibraryData = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
||||||
version: VERSIONS.excalidrawLibrary,
|
version: VERSIONS.excalidrawLibrary,
|
||||||
source: EXPORT_SOURCE,
|
source: getExportSource(),
|
||||||
libraryItems: clonedLibItems,
|
libraryItems: clonedLibItems,
|
||||||
};
|
};
|
||||||
const content = JSON.stringify(libContent, null, 2);
|
const content = JSON.stringify(libContent, null, 2);
|
||||||
|
@ -4,8 +4,7 @@ import { ButtonIcon } from "./ButtonIcon";
|
|||||||
|
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
|
||||||
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
export const RadioSelection = <T extends Object>(
|
||||||
export const ButtonIconSelect = <T extends Object>(
|
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
value: T;
|
value: T;
|
||||||
@ -17,7 +16,6 @@ export const ButtonIconSelect = <T extends Object>(
|
|||||||
}[];
|
}[];
|
||||||
value: T | null;
|
value: T | null;
|
||||||
type?: "radio" | "button";
|
type?: "radio" | "button";
|
||||||
children?: React.ReactNode;
|
|
||||||
} & (
|
} & (
|
||||||
| { type?: "radio"; group: string; onChange: (value: T) => void }
|
| { type?: "radio"; group: string; onChange: (value: T) => void }
|
||||||
| {
|
| {
|
||||||
@ -29,7 +27,7 @@ export const ButtonIconSelect = <T extends Object>(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
) => (
|
) => (
|
||||||
<div className="buttonList">
|
<>
|
||||||
{props.options.map((option) =>
|
{props.options.map((option) =>
|
||||||
props.type === "button" ? (
|
props.type === "button" ? (
|
||||||
<ButtonIcon
|
<ButtonIcon
|
||||||
@ -57,6 +55,5 @@ export const ButtonIconSelect = <T extends Object>(
|
|||||||
</label>
|
</label>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{props.children}
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
@ -4,8 +4,6 @@ import { MIME_TYPES } from "@excalidraw/common";
|
|||||||
import { getElementAbsoluteCoords } from "@excalidraw/element";
|
import { getElementAbsoluteCoords } from "@excalidraw/element";
|
||||||
import { hitElementBoundingBox } from "@excalidraw/element";
|
import { hitElementBoundingBox } from "@excalidraw/element";
|
||||||
|
|
||||||
import { DEFAULT_LINK_SIZE } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Bounds } from "@excalidraw/element";
|
import type { Bounds } from "@excalidraw/element";
|
||||||
@ -16,9 +14,11 @@ import type {
|
|||||||
|
|
||||||
import type { AppState, UIAppState } from "../../types";
|
import type { AppState, UIAppState } from "../../types";
|
||||||
|
|
||||||
|
export const DEFAULT_LINK_SIZE = 12;
|
||||||
|
|
||||||
export const EXTERNAL_LINK_IMG = document.createElement("img");
|
export const EXTERNAL_LINK_IMG = document.createElement("img");
|
||||||
EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
|
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
export const ELEMENT_LINK_IMG = document.createElement("img");
|
export const ELEMENT_LINK_IMG = document.createElement("img");
|
||||||
@ -32,13 +32,14 @@ export const getLinkHandleFromCoords = (
|
|||||||
appState: Pick<UIAppState, "zoom">,
|
appState: Pick<UIAppState, "zoom">,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
const size = DEFAULT_LINK_SIZE;
|
const size = DEFAULT_LINK_SIZE;
|
||||||
const linkWidth = size / appState.zoom.value;
|
const zoom = appState.zoom.value > 1 ? appState.zoom.value : 1;
|
||||||
const linkHeight = size / appState.zoom.value;
|
const linkWidth = size / zoom;
|
||||||
const linkMarginY = size / appState.zoom.value;
|
const linkHeight = size / zoom;
|
||||||
|
const linkMarginY = size / zoom;
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
const centeringOffset = (size - 8) / (2 * appState.zoom.value);
|
const centeringOffset = (size - 8) / (2 * zoom);
|
||||||
const dashedLineMargin = 4 / appState.zoom.value;
|
const dashedLineMargin = 4 / zoom;
|
||||||
|
|
||||||
// Same as `ne` resize handle
|
// Same as `ne` resize handle
|
||||||
const x = x2 + dashedLineMargin - centeringOffset;
|
const x = x2 + dashedLineMargin - centeringOffset;
|
||||||
|
@ -140,7 +140,7 @@ body.excalidraw-cursor-resize * {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelColumn {
|
.selected-shape-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
row-gap: 0.75rem;
|
row-gap: 0.75rem;
|
||||||
@ -245,10 +245,6 @@ body.excalidraw-cursor-resize * {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
--bar-padding: calc(4 * var(--space-factor));
|
--bar-padding: calc(4 * var(--space-factor));
|
||||||
padding-top: #{"max(var(--bar-padding), var(--sat,0))"};
|
|
||||||
padding-right: var(--sar, 0);
|
|
||||||
padding-bottom: var(--sab, 0);
|
|
||||||
padding-left: var(--sal, 0);
|
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
@ -263,10 +259,6 @@ body.excalidraw-cursor-resize * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
pointer-events: var(--ui-pointerEvents);
|
pointer-events: var(--ui-pointerEvents);
|
||||||
|
|
||||||
.panelColumn {
|
|
||||||
padding: 8px 8px 0 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +294,10 @@ body.excalidraw-cursor-resize * {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-bottom: var(--bar-padding);
|
margin-bottom: var(--bar-padding);
|
||||||
|
|
||||||
|
.selected-shape-actions {
|
||||||
|
padding: 8px 8px 0 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-menu {
|
.App-menu {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_FILENAME,
|
DEFAULT_FILENAME,
|
||||||
EXPORT_DATA_TYPES,
|
EXPORT_DATA_TYPES,
|
||||||
EXPORT_SOURCE,
|
getExportSource,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
VERSIONS,
|
VERSIONS,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
@ -56,7 +56,7 @@ export const serializeAsJSON = (
|
|||||||
const data: ExportedDataState = {
|
const data: ExportedDataState = {
|
||||||
type: EXPORT_DATA_TYPES.excalidraw,
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
version: VERSIONS.excalidraw,
|
version: VERSIONS.excalidraw,
|
||||||
source: EXPORT_SOURCE,
|
source: getExportSource(),
|
||||||
elements:
|
elements:
|
||||||
type === "local"
|
type === "local"
|
||||||
? clearElementsForExport(elements)
|
? clearElementsForExport(elements)
|
||||||
@ -142,7 +142,7 @@ export const serializeLibraryAsJSON = (libraryItems: LibraryItems) => {
|
|||||||
const data: ExportedLibraryData = {
|
const data: ExportedLibraryData = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
||||||
version: VERSIONS.excalidrawLibrary,
|
version: VERSIONS.excalidrawLibrary,
|
||||||
source: EXPORT_SOURCE,
|
source: getExportSource(),
|
||||||
libraryItems,
|
libraryItems,
|
||||||
};
|
};
|
||||||
return JSON.stringify(data, null, 2);
|
return JSON.stringify(data, null, 2);
|
||||||
|
@ -17,7 +17,7 @@ import { selectGroupsForSelectedElements } from "@excalidraw/element";
|
|||||||
|
|
||||||
import { getContainerElement } from "@excalidraw/element";
|
import { getContainerElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import { arrayToMap, easeOut } from "@excalidraw/common";
|
import { arrayToMap, easeOut, isShallowEqual } from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@ -33,11 +33,18 @@ import { getLassoSelectedElementIds } from "./utils";
|
|||||||
|
|
||||||
import type App from "../components/App";
|
import type App from "../components/App";
|
||||||
|
|
||||||
|
type CanvasTranslate = {
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
|
zoom: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class LassoTrail extends AnimatedTrail {
|
export class LassoTrail extends AnimatedTrail {
|
||||||
private intersectedElements: Set<ExcalidrawElement["id"]> = new Set();
|
private intersectedElements: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
private enclosedElements: Set<ExcalidrawElement["id"]> = new Set();
|
private enclosedElements: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
private elementsSegments: Map<string, LineSegment<GlobalPoint>[]> | null =
|
private elementsSegments: Map<string, LineSegment<GlobalPoint>[]> | null =
|
||||||
null;
|
null;
|
||||||
|
private canvasTranslate: CanvasTranslate | null = null;
|
||||||
private keepPreviousSelection: boolean = false;
|
private keepPreviousSelection: boolean = false;
|
||||||
|
|
||||||
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||||
@ -169,7 +176,17 @@ export class LassoTrail extends AnimatedTrail {
|
|||||||
.getCurrentTrail()
|
.getCurrentTrail()
|
||||||
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1]));
|
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1]));
|
||||||
|
|
||||||
if (!this.elementsSegments) {
|
const currentCanvasTranslate: CanvasTranslate = {
|
||||||
|
scrollX: this.app.state.scrollX,
|
||||||
|
scrollY: this.app.state.scrollY,
|
||||||
|
zoom: this.app.state.zoom.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.elementsSegments ||
|
||||||
|
!isShallowEqual(currentCanvasTranslate, this.canvasTranslate ?? {})
|
||||||
|
) {
|
||||||
|
this.canvasTranslate = currentCanvasTranslate;
|
||||||
this.elementsSegments = new Map();
|
this.elementsSegments = new Map();
|
||||||
const visibleElementsMap = arrayToMap(this.app.visibleElements);
|
const visibleElementsMap = arrayToMap(this.app.visibleElements);
|
||||||
for (const element of this.app.visibleElements) {
|
for (const element of this.app.visibleElements) {
|
||||||
|
@ -188,7 +188,7 @@ const renderLinkIcon = (
|
|||||||
window.devicePixelRatio * appState.zoom.value,
|
window.devicePixelRatio * appState.zoom.value,
|
||||||
window.devicePixelRatio * appState.zoom.value,
|
window.devicePixelRatio * appState.zoom.value,
|
||||||
);
|
);
|
||||||
linkCanvasCacheContext.fillStyle = "#fff";
|
linkCanvasCacheContext.fillStyle = appState.viewBackgroundColor || "#fff";
|
||||||
linkCanvasCacheContext.fillRect(0, 0, width, height);
|
linkCanvasCacheContext.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
if (canvasKey === "elementLink") {
|
if (canvasKey === "elementLink") {
|
||||||
|
@ -170,10 +170,11 @@ export const isSnappingEnabled = ({
|
|||||||
}) => {
|
}) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
return (
|
return (
|
||||||
(app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) ||
|
app.state.activeTool.type !== "lasso" &&
|
||||||
(!app.state.objectsSnapModeEnabled &&
|
((app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) ||
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
(!app.state.objectsSnapModeEnabled &&
|
||||||
!isGridModeEnabled(app))
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
!isGridModeEnabled(app)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1324,7 +1324,7 @@ describe("textWysiwyg", () => {
|
|||||||
).toEqual(FONT_FAMILY.Nunito);
|
).toEqual(FONT_FAMILY.Nunito);
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
).toEqual(1.35);
|
).toEqual(1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should align correctly", () => {
|
describe("should align correctly", () => {
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"default": "./dist/prod/index.js"
|
"default": "./dist/prod/index.js"
|
||||||
},
|
},
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./../math/dist/types/math/src/*.d.ts"
|
"types": "./dist/types/math/src/*.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"default": "./dist/prod/index.js"
|
"default": "./dist/prod/index.js"
|
||||||
},
|
},
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./../utils/dist/types/utils/src/*.d.ts"
|
"types": "./dist/types/utils/src/*.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user