Compare commits

..

4 Commits

Author SHA1 Message Date
zsviczian
3ad69c661f updated export.test 2023-06-16 22:24:44 +02:00
zsviczian
56ea3c5bd0 rounded images 2023-06-16 22:09:28 +02:00
Aakansha Doshi
6de6a96abf docs: add info about roadmap (#6687) 2023-06-16 20:55:33 +05:30
Sudharsan Aravind
28ab6531c9 fix: updated link for documentation page under help section (#6654)
* fix: updated link for documentation page under help section

* Update docs link

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-06-15 14:58:11 +05:30
11 changed files with 63 additions and 131 deletions

View File

@@ -2,6 +2,11 @@
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you.
For new contributors we would recommend to start with *Easy* tasks.
In case you want to pick up something from the roadmap, comment on that issue and one of the project maintainers will assign it to you, post which you can discuss in the issue and start working on it.
## Setup
### Option 1 - Manual

View File

@@ -2,10 +2,10 @@ import React from "react";
import {
Action,
UpdaterFn,
ActionName,
ActionResult,
PanelComponentProps,
ActionSource,
ActionPredicateFn,
} from "./types";
import { ExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
@@ -40,8 +40,7 @@ const trackAction = (
};
export class ActionManager {
actions = {} as Record<Action["name"], Action>;
actionPredicates = [] as ActionPredicateFn[];
actions = {} as Record<ActionName, Action>;
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
@@ -69,12 +68,6 @@ export class ActionManager {
this.app = app;
}
registerActionPredicate(predicate: ActionPredicateFn) {
if (!this.actionPredicates.includes(predicate)) {
this.actionPredicates.push(predicate);
}
}
registerAction(action: Action) {
this.actions[action.name] = action;
}
@@ -91,7 +84,7 @@ export class ActionManager {
(action) =>
(action.name in canvasActions
? canvasActions[action.name as keyof typeof canvasActions]
: this.isActionEnabled(action, { noPredicates: true })) &&
: true) &&
action.keyTest &&
action.keyTest(
event,
@@ -141,7 +134,7 @@ export class ActionManager {
/**
* @param data additional data sent to the PanelComponent
*/
renderAction = (name: Action["name"], data?: PanelComponentProps["data"]) => {
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
const canvasActions = this.app.props.UIOptions.canvasActions;
if (
@@ -149,7 +142,7 @@ export class ActionManager {
"PanelComponent" in this.actions[name] &&
(name in canvasActions
? canvasActions[name as keyof typeof canvasActions]
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
: true)
) {
const action = this.actions[name];
const PanelComponent = action.PanelComponent!;
@@ -183,31 +176,13 @@ export class ActionManager {
return null;
};
isActionEnabled = (
action: Action,
opts?: {
elements?: readonly ExcalidrawElement[];
data?: Record<string, any>;
noPredicates?: boolean;
},
): boolean => {
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
isActionEnabled = (action: Action) => {
const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState();
const data = opts?.data;
if (
!opts?.noPredicates &&
action.predicate &&
!action.predicate(elements, appState, this.app.props, this.app, data)
) {
return false;
}
let enabled = true;
this.actionPredicates.forEach((fn) => {
if (!fn(action, elements, appState, data)) {
enabled = false;
}
});
return enabled;
return (
!action.predicate ||
action.predicate(elements, appState, this.app.props, this.app)
);
};
}

View File

@@ -32,15 +32,6 @@ type ActionFn = (
app: AppClassProperties,
) => ActionResult | Promise<ActionResult>;
// Return `true` *unless* `Action` should be disabled
// given `elements`, `appState`, and optionally `data`.
export type ActionPredicateFn = (
action: Action,
elements: readonly ExcalidrawElement[],
appState: AppState,
data?: Record<string, any>,
) => boolean;
export type UpdaterFn = (res: ActionResult) => void;
export type ActionFilterFn = (action: Action) => void;
@@ -141,7 +132,7 @@ export type PanelComponentProps = {
};
export interface Action {
name: string;
name: ActionName;
PanelComponent?: React.FC<PanelComponentProps>;
perform: ActionFn;
keyPriority?: number;
@@ -161,7 +152,6 @@ export interface Action {
appState: AppState,
appProps: ExcalidrawProps,
app: AppClassProperties,
data?: Record<string, any>,
) => boolean;
checked?: (appState: Readonly<AppState>) => boolean;
trackEvent:

View File

@@ -479,12 +479,6 @@ class App extends React.Component<AppProps, AppState> {
this.id = nanoid();
this.library = new Library(this);
this.actionManager = new ActionManager(
this.syncActionResult,
() => this.state,
() => this.scene.getElementsIncludingDeleted(),
this,
);
if (excalidrawRef) {
const readyPromise =
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
@@ -505,7 +499,6 @@ class App extends React.Component<AppProps, AppState> {
getSceneElements: this.getSceneElements,
getAppState: () => this.state,
getFiles: () => this.files,
actionManager: this.actionManager,
refresh: this.refresh,
setToast: this.setToast,
id: this.id,
@@ -534,6 +527,12 @@ class App extends React.Component<AppProps, AppState> {
onSceneUpdated: this.onSceneUpdated,
});
this.history = new History();
this.actionManager = new ActionManager(
this.syncActionResult,
() => this.state,
() => this.scene.getElementsIncludingDeleted(),
this,
);
this.actionManager.registerAll(actions);
this.actionManager.registerAction(createUndoAction(this.history));

View File

@@ -12,7 +12,7 @@ const Header = () => (
<div className="HelpDialog__header">
<a
className="HelpDialog__btn"
href="https://github.com/excalidraw/excalidraw#documentation"
href="https://docs.excalidraw.com"
target="_blank"
rel="noopener noreferrer"
>

View File

@@ -43,3 +43,7 @@ Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/
## API
Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api)
## Contributing
Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/contributing)

View File

@@ -251,6 +251,7 @@ const drawImagePlaceholder = (
size,
);
};
const drawElementOnCanvas = (
element: NonDeletedExcalidrawElement,
rc: RoughCanvas,
@@ -300,17 +301,35 @@ const drawElementOnCanvas = (
const img = isInitializedImageElement(element)
? renderConfig.imageCache.get(element.fileId)?.image
: undefined;
context.save();
const { width, height } = element;
if (element.roundness) {
const r = getCornerRadius(Math.min(width, height), element) / 4;
context.beginPath();
context.moveTo(r, 0);
context.lineTo(width - r, 0);
context.quadraticCurveTo(width, 0, width, r);
context.lineTo(width, height - r);
context.quadraticCurveTo(width, height, width - r, height);
context.lineTo(r, height);
context.quadraticCurveTo(0, height, 0, height - r);
context.lineTo(0, r);
context.quadraticCurveTo(0, 0, r, 0);
context.closePath();
context.clip();
}
if (img != null && !(img instanceof Promise)) {
context.drawImage(
img,
0 /* hardcoded for the selection box*/,
0,
element.width,
element.height,
width,
height,
);
} else {
drawImagePlaceholder(element, context, renderConfig.zoom.value);
}
context.restore();
break;
}
default: {
@@ -1390,7 +1409,14 @@ export const renderElementToSvg = (
image.setAttribute("width", "100%");
image.setAttribute("height", "100%");
image.setAttribute("href", fileData.dataURL);
if (element.roundness) {
const r =
getCornerRadius(
Math.min(element.width, element.height),
element,
) / 4;
image.setAttribute("clip-path", `inset(0% round ${r}px)`);
}
symbol.appendChild(image);
root.prepend(symbol);

View File

@@ -29,7 +29,8 @@ export const canChangeRoundness = (type: string) =>
type === "rectangle" ||
type === "arrow" ||
type === "line" ||
type === "diamond";
type === "diamond" ||
type === "image";
export const hasText = (type: string) => type === "text";

View File

@@ -1,71 +0,0 @@
import { ExcalidrawElement } from "../element/types";
import { API } from "./helpers/api";
import { render } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app";
import { Action, ActionPredicateFn, ActionResult } from "../actions/types";
import {
actionChangeFontFamily,
actionChangeFontSize,
} from "../actions/actionProperties";
import { isTextElement } from "../element";
const { h } = window;
describe("regression tests", () => {
it("should apply universal action predicates", async () => {
await render(<ExcalidrawApp />);
// Create the test elements
const el1 = API.createElement({ type: "rectangle", id: "A", y: 0 });
const el2 = API.createElement({ type: "rectangle", id: "B", y: 30 });
const el3 = API.createElement({ type: "text", id: "C", y: 60 });
const el12: ExcalidrawElement[] = [el1, el2];
const el13: ExcalidrawElement[] = [el1, el3];
const el23: ExcalidrawElement[] = [el2, el3];
const el123: ExcalidrawElement[] = [el1, el2, el3];
// Set up the custom Action enablers
const enableName = "custom" as Action["name"];
const enableAction: Action = {
name: enableName,
perform: (): ActionResult => {
return {} as ActionResult;
},
trackEvent: false,
};
const enabler: ActionPredicateFn = function (action, elements) {
if (action.name !== enableName || elements.some((el) => el.y === 30)) {
return true;
}
return false;
};
// Set up the standard Action disablers
const disabled1 = actionChangeFontFamily;
const disabled2 = actionChangeFontSize;
const disabler: ActionPredicateFn = function (action, elements) {
if (
action.name === disabled2.name &&
elements.some((el) => el.y === 0 || isTextElement(el))
) {
return false;
}
return true;
};
// Test the custom Action enablers
const am = h.app.actionManager;
am.registerActionPredicate(enabler);
expect(am.isActionEnabled(enableAction, { elements: el12 })).toBe(true);
expect(am.isActionEnabled(enableAction, { elements: el13 })).toBe(false);
expect(am.isActionEnabled(enableAction, { elements: el23 })).toBe(true);
expect(am.isActionEnabled(disabled1, { elements: el12 })).toBe(true);
expect(am.isActionEnabled(disabled1, { elements: el13 })).toBe(true);
expect(am.isActionEnabled(disabled1, { elements: el23 })).toBe(true);
// Test the standard Action disablers
am.registerActionPredicate(disabler);
expect(am.isActionEnabled(disabled1, { elements: el123 })).toBe(true);
expect(am.isActionEnabled(disabled2, { elements: [el1] })).toBe(false);
expect(am.isActionEnabled(disabled2, { elements: [el2] })).toBe(true);
expect(am.isActionEnabled(disabled2, { elements: [el3] })).toBe(false);
expect(am.isActionEnabled(disabled2, { elements: el12 })).toBe(false);
expect(am.isActionEnabled(disabled2, { elements: el23 })).toBe(false);
expect(am.isActionEnabled(disabled2, { elements: el13 })).toBe(false);
});
});

View File

@@ -118,6 +118,7 @@ describe("export", () => {
scale: [1, 1],
width: 100,
height: 100,
roundness: null,
angle: normalizeAngle(315),
}),
API.createElement({
@@ -128,6 +129,7 @@ describe("export", () => {
scale: [-1, 1],
width: 50,
height: 50,
roundness: null,
angle: normalizeAngle(45),
}),
API.createElement({
@@ -138,6 +140,7 @@ describe("export", () => {
scale: [1, -1],
width: 100,
height: 100,
roundness: null,
angle: normalizeAngle(45),
}),
API.createElement({
@@ -148,6 +151,7 @@ describe("export", () => {
scale: [-1, -1],
width: 50,
height: 50,
roundness: null,
angle: normalizeAngle(315),
}),
];

View File

@@ -528,7 +528,6 @@ export type ExcalidrawImperativeAPI = {
getSceneElements: InstanceType<typeof App>["getSceneElements"];
getAppState: () => InstanceType<typeof App>["state"];
getFiles: () => InstanceType<typeof App>["files"];
actionManager: InstanceType<typeof App>["actionManager"];
refresh: InstanceType<typeof App>["refresh"];
setToast: InstanceType<typeof App>["setToast"];
addFiles: (data: BinaryFileData[]) => void;