feat: Element locking (#4964)

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Zsolt Viczian <viczian.zsolt@gmail.com>
This commit is contained in:
Tom Sherman 2022-04-07 12:43:29 +01:00 committed by GitHub
parent c2fce6d8c4
commit 327ed0e2d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1066 additions and 53 deletions

View file

@ -94,6 +94,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -149,6 +150,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -268,6 +270,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -296,6 +299,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -351,6 +355,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -390,6 +395,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -415,6 +421,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -454,6 +461,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -479,6 +487,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -598,6 +607,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -626,6 +636,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -681,6 +692,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -720,6 +732,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -745,6 +758,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -784,6 +798,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -809,6 +824,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -928,6 +944,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -983,6 +1000,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -1100,6 +1118,7 @@ Object {
"id": "id0",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1155,6 +1174,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1192,6 +1212,7 @@ Object {
"id": "id0",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1311,6 +1332,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1339,6 +1361,7 @@ Object {
"id": "id0_copy",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -1394,6 +1417,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1433,6 +1457,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1458,6 +1483,7 @@ Object {
"id": "id0_copy",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -1585,6 +1611,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1615,6 +1642,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -1670,6 +1698,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1709,6 +1738,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1734,6 +1764,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -1779,6 +1810,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -1806,6 +1838,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -1925,6 +1958,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 60,
"roughness": 2,
"seed": 1278240551,
@ -1953,6 +1987,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 60,
"roughness": 2,
"seed": 400692809,
@ -2008,6 +2043,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2047,6 +2083,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2072,6 +2109,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2111,6 +2149,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2136,6 +2175,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2175,6 +2215,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2200,6 +2241,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2239,6 +2281,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2264,6 +2307,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2303,6 +2347,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2328,6 +2373,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2367,6 +2413,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2392,6 +2439,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2431,6 +2479,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2456,6 +2505,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 2,
"seed": 400692809,
@ -2495,6 +2545,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2520,6 +2571,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 60,
"roughness": 2,
"seed": 400692809,
@ -2559,6 +2611,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 60,
"roughness": 2,
"seed": 1278240551,
@ -2584,6 +2637,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 60,
"roughness": 2,
"seed": 400692809,
@ -2703,6 +2757,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2731,6 +2786,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2786,6 +2842,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2825,6 +2882,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -2850,6 +2908,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2889,6 +2948,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -2914,6 +2974,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -3033,6 +3094,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -3061,6 +3123,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -3116,6 +3179,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -3155,6 +3219,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -3180,6 +3245,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -3219,6 +3285,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -3244,6 +3311,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -3367,6 +3435,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -3395,6 +3464,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 401146281,
@ -3450,6 +3520,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -3489,6 +3560,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -3514,6 +3586,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 401146281,
@ -3559,6 +3632,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -3586,6 +3660,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 401146281,
@ -3627,6 +3702,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -3652,6 +3728,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 401146281,
@ -3777,6 +3854,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -3805,6 +3883,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -3860,6 +3939,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -3899,6 +3979,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -3924,6 +4005,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 453191,
@ -4053,6 +4135,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -4083,6 +4166,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 401146281,
@ -4138,6 +4222,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -4177,6 +4262,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -4202,6 +4288,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 401146281,
@ -4248,6 +4335,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -4275,6 +4363,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 401146281,
@ -4582,6 +4671,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -4610,6 +4700,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
@ -4638,6 +4729,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,
@ -4693,6 +4785,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 1278240551,

View file

@ -16,6 +16,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
@ -58,6 +59,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
@ -88,6 +90,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
@ -119,6 +122,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
@ -161,6 +165,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,

View file

@ -11,6 +11,7 @@ Object {
"id": "id0_copy",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 401146281,
@ -39,6 +40,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
@ -67,6 +69,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
@ -100,6 +103,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
@ -133,6 +137,7 @@ Object {
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
@ -168,6 +173,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [

View file

@ -17,6 +17,7 @@ Object {
110,
],
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
@ -67,6 +68,7 @@ Object {
110,
],
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
@ -57,6 +58,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
@ -97,6 +99,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
@ -125,6 +128,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
@ -153,6 +157,7 @@ Object {
"id": "id0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,

View file

@ -36,10 +36,6 @@ const checkpoint = (name: string) => {
const mouse = new Pointer("mouse");
const queryContextMenu = () => {
return GlobalTestState.renderResult.container.querySelector(".context-menu");
};
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@ -83,7 +79,7 @@ describe("contextMenu element", () => {
clientX: 1,
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
const contextMenuOptions =
contextMenu?.querySelectorAll(".context-menu li");
const expectedShortcutNames: ShortcutName[] = [
@ -113,7 +109,7 @@ describe("contextMenu element", () => {
clientX: 1,
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
const contextMenuOptions =
contextMenu?.querySelectorAll(".context-menu li");
const expectedShortcutNames: ShortcutName[] = [
@ -129,6 +125,7 @@ describe("contextMenu element", () => {
"bringToFront",
"duplicateSelection",
"hyperlink",
"toggleLock",
];
expect(contextMenu).not.toBeNull();
@ -166,7 +163,7 @@ describe("contextMenu element", () => {
clientX: 100,
clientY: 100,
});
expect(queryContextMenu()).not.toBeNull();
expect(UI.queryContextMenu()).not.toBeNull();
expect(API.getSelectedElement().id).toBe(rect1.id);
// higher z-index
@ -176,7 +173,7 @@ describe("contextMenu element", () => {
clientX: 100,
clientY: 100,
});
expect(queryContextMenu()).not.toBeNull();
expect(UI.queryContextMenu()).not.toBeNull();
expect(API.getSelectedElement().id).toBe(rect2.id);
});
@ -201,7 +198,7 @@ describe("contextMenu element", () => {
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
const contextMenuOptions =
contextMenu?.querySelectorAll(".context-menu li");
const expectedShortcutNames: ShortcutName[] = [
@ -215,6 +212,7 @@ describe("contextMenu element", () => {
"sendToBack",
"bringToFront",
"duplicateSelection",
"toggleLock",
];
expect(contextMenu).not.toBeNull();
@ -251,7 +249,7 @@ describe("contextMenu element", () => {
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
const contextMenuOptions =
contextMenu?.querySelectorAll(".context-menu li");
const expectedShortcutNames: ShortcutName[] = [
@ -265,6 +263,7 @@ describe("contextMenu element", () => {
"sendToBack",
"bringToFront",
"duplicateSelection",
"toggleLock",
];
expect(contextMenu).not.toBeNull();
@ -286,7 +285,7 @@ describe("contextMenu element", () => {
clientX: 1,
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
expect(copiedStyles).toBe("{}");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
expect(copiedStyles).not.toBe("{}");
@ -328,7 +327,7 @@ describe("contextMenu element", () => {
clientX: 40,
clientY: 40,
});
let contextMenu = queryContextMenu();
let contextMenu = UI.queryContextMenu();
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
const secondRect = JSON.parse(copiedStyles);
expect(secondRect.id).toBe(h.elements[1].id);
@ -340,7 +339,7 @@ describe("contextMenu element", () => {
clientX: 10,
clientY: 10,
});
contextMenu = queryContextMenu();
contextMenu = UI.queryContextMenu();
fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);
const firstRect = API.getSelectedElement();
@ -364,7 +363,7 @@ describe("contextMenu element", () => {
clientX: 1,
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
fireEvent.click(queryAllByText(contextMenu as HTMLElement, "Delete")[0]);
expect(API.getSelectedElements()).toHaveLength(0);
expect(h.elements[0].isDeleted).toBe(true);
@ -380,7 +379,7 @@ describe("contextMenu element", () => {
clientX: 1,
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
fireEvent.click(queryByText(contextMenu as HTMLElement, "Add to library")!);
await waitFor(() => {
@ -401,7 +400,7 @@ describe("contextMenu element", () => {
clientX: 1,
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
fireEvent.click(queryByText(contextMenu as HTMLElement, "Duplicate")!);
expect(h.elements).toHaveLength(2);
const { id: _id0, seed: _seed0, x: _x0, y: _y0, ...rect1 } = h.elements[0];
@ -424,7 +423,7 @@ describe("contextMenu element", () => {
clientX: 40,
clientY: 40,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
const elementsBefore = h.elements;
fireEvent.click(queryByText(contextMenu as HTMLElement, "Send backward")!);
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
@ -446,7 +445,7 @@ describe("contextMenu element", () => {
clientX: 10,
clientY: 10,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
const elementsBefore = h.elements;
fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring forward")!);
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
@ -468,7 +467,7 @@ describe("contextMenu element", () => {
clientX: 40,
clientY: 40,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
const elementsBefore = h.elements;
fireEvent.click(queryByText(contextMenu as HTMLElement, "Send to back")!);
expect(elementsBefore[1].id).toEqual(h.elements[0].id);
@ -489,7 +488,7 @@ describe("contextMenu element", () => {
clientX: 10,
clientY: 10,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
const elementsBefore = h.elements;
fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring to front")!);
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
@ -514,7 +513,7 @@ describe("contextMenu element", () => {
clientX: 1,
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
fireEvent.click(
queryByText(contextMenu as HTMLElement, "Group selection")!,
);
@ -547,7 +546,7 @@ describe("contextMenu element", () => {
clientY: 1,
});
const contextMenu = queryContextMenu();
const contextMenu = UI.queryContextMenu();
expect(contextMenu).not.toBeNull();
fireEvent.click(
queryByText(contextMenu as HTMLElement, "Ungroup selection")!,

View file

@ -14,6 +14,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
@ -58,6 +59,7 @@ Object {
"id": "1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 10,
"roughness": 2,
"seed": Any<Number>,
@ -90,6 +92,7 @@ Object {
"id": "2",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 10,
"roughness": 2,
"seed": Any<Number>,
@ -122,6 +125,7 @@ Object {
"id": "3",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 10,
"roughness": 2,
"seed": Any<Number>,
@ -151,6 +155,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [],
"pressures": Array [],
@ -185,6 +190,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
@ -228,6 +234,7 @@ Object {
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
@ -272,6 +279,7 @@ Object {
"id": "id-text01",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"originalText": "text",
"roughness": 1,
@ -308,6 +316,7 @@ Object {
"id": "id-text01",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"originalText": "test",
"roughness": 1,

View file

@ -0,0 +1,388 @@
import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app";
import { render } from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { KEYS } from "../keys";
import { API } from "../tests/helpers/api";
import { actionSelectAll } from "../actions";
import { t } from "../i18n";
import { mutateElement } from "../element/mutateElement";
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const mouse = new Pointer("mouse");
const h = window.h;
describe("element locking", () => {
beforeEach(async () => {
await render(<ExcalidrawApp />);
h.elements = [];
});
it("click-selecting a locked element is disabled", () => {
const lockedRectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
locked: true,
});
h.elements = [lockedRectangle];
mouse.clickAt(50, 50);
expect(API.getSelectedElements().length).toBe(0);
});
it("box-selecting a locked element is disabled", () => {
const lockedRectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
locked: true,
x: 100,
y: 100,
});
h.elements = [lockedRectangle];
mouse.downAt(50, 50);
mouse.moveTo(250, 250);
mouse.upAt(250, 250);
expect(API.getSelectedElements().length).toBe(0);
});
it("dragging a locked element is disabled", () => {
const lockedRectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
locked: true,
});
h.elements = [lockedRectangle];
mouse.downAt(50, 50);
mouse.moveTo(100, 100);
mouse.upAt(100, 100);
expect(lockedRectangle).toEqual(expect.objectContaining({ x: 0, y: 0 }));
});
it("you can drag element that's below a locked element", () => {
const rectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
});
const lockedRectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
locked: true,
});
h.elements = [rectangle, lockedRectangle];
mouse.downAt(50, 50);
mouse.moveTo(100, 100);
mouse.upAt(100, 100);
expect(lockedRectangle).toEqual(expect.objectContaining({ x: 0, y: 0 }));
expect(rectangle).toEqual(expect.objectContaining({ x: 50, y: 50 }));
expect(API.getSelectedElements().length).toBe(1);
expect(API.getSelectedElement().id).toBe(rectangle.id);
});
it("selectAll shouldn't select locked elements", () => {
h.elements = [
API.createElement({ type: "rectangle" }),
API.createElement({ type: "rectangle", locked: true }),
];
h.app.actionManager.executeAction(actionSelectAll);
expect(API.getSelectedElements().length).toBe(1);
});
it("clicking on a locked element should select the unlocked element beneath it", () => {
const rectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
});
const lockedRectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
locked: true,
});
h.elements = [rectangle, lockedRectangle];
expect(API.getSelectedElements().length).toBe(0);
mouse.clickAt(50, 50);
expect(API.getSelectedElements().length).toBe(1);
expect(API.getSelectedElement().id).toBe(rectangle.id);
});
it("right-clicking on a locked element should select it & open its contextMenu", () => {
const rectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
});
const lockedRectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
locked: true,
});
h.elements = [rectangle, lockedRectangle];
expect(API.getSelectedElements().length).toBe(0);
mouse.rightClickAt(50, 50);
expect(API.getSelectedElements().length).toBe(1);
expect(API.getSelectedElement().id).toBe(lockedRectangle.id);
const contextMenu = UI.queryContextMenu();
expect(contextMenu).not.toBeNull();
expect(
contextMenu?.querySelector(
`li[data-testid="toggleLock"] .context-menu-option__label`,
),
).toHaveTextContent(t("labels.elementLock.unlock"));
});
it("right-clicking on element covered by locked element should ignore the locked element", () => {
const rectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
});
const lockedRectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
locked: true,
});
h.elements = [rectangle, lockedRectangle];
API.setSelectedElements([rectangle]);
expect(API.getSelectedElements().length).toBe(1);
expect(API.getSelectedElement().id).toBe(rectangle.id);
mouse.rightClickAt(50, 50);
expect(API.getSelectedElements().length).toBe(1);
expect(API.getSelectedElement().id).toBe(rectangle.id);
const contextMenu = UI.queryContextMenu();
expect(contextMenu).not.toBeNull();
});
it("selecting a group selects all elements including locked ones", () => {
const rectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
groupIds: ["g1"],
});
const lockedRectangle = API.createElement({
type: "rectangle",
width: 100,
backgroundColor: "red",
fillStyle: "solid",
locked: true,
groupIds: ["g1"],
x: 200,
y: 200,
});
h.elements = [rectangle, lockedRectangle];
mouse.clickAt(250, 250);
expect(API.getSelectedElements().length).toBe(0);
mouse.clickAt(50, 50);
expect(API.getSelectedElements().length).toBe(2);
});
it("should ignore locked text element in center of container on ENTER", () => {
const container = API.createElement({
type: "rectangle",
width: 100,
});
const textSize = 20;
const text = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
locked: true,
});
h.elements = [container, text];
API.setSelectedElements([container]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).not.toBe(text.id);
expect(h.state.editingElement?.id).toBe(h.elements[2].id);
});
it("should ignore locked text under cursor when clicked with text tool", () => {
const text = API.createElement({
type: "text",
text: "ola",
x: 60,
y: 0,
width: 100,
height: 100,
locked: true,
});
h.elements = [text];
UI.clickTool("text");
mouse.clickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).not.toBe(text.id);
expect(h.elements.length).toBe(2);
expect(h.state.editingElement?.id).toBe(h.elements[1].id);
});
it("should ignore text under cursor when double-clicked with selection tool", () => {
const text = API.createElement({
type: "text",
text: "ola",
x: 60,
y: 0,
width: 100,
height: 100,
locked: true,
});
h.elements = [text];
UI.clickTool("selection");
mouse.doubleClickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).not.toBe(text.id);
expect(h.elements.length).toBe(2);
expect(h.state.editingElement?.id).toBe(h.elements[1].id);
});
it("locking should include bound text", () => {
const container = API.createElement({
type: "rectangle",
width: 100,
});
const textSize = 20;
const text = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
mutateElement(container, {
boundElements: [{ id: text.id, type: "text" }],
});
h.elements = [container, text];
UI.clickTool("selection");
mouse.clickAt(container.x + 10, container.y + 10);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.L);
});
expect(h.elements).toEqual([
expect.objectContaining({
id: container.id,
locked: true,
}),
expect.objectContaining({
id: text.id,
locked: true,
}),
]);
});
it("bound text shouldn't be editable via double-click", () => {
const container = API.createElement({
type: "rectangle",
width: 100,
locked: true,
});
const textSize = 20;
const text = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
locked: true,
});
mutateElement(container, {
boundElements: [{ id: text.id, type: "text" }],
});
h.elements = [container, text];
UI.clickTool("selection");
mouse.doubleClickAt(container.width / 2, container.height / 2);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).not.toBe(text.id);
expect(h.elements.length).toBe(3);
expect(h.state.editingElement?.id).toBe(h.elements[2].id);
});
it("bound text shouldn't be editable via text tool", () => {
const container = API.createElement({
type: "rectangle",
width: 100,
locked: true,
});
const textSize = 20;
const text = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
locked: true,
});
mutateElement(container, {
boundElements: [{ id: text.id, type: "text" }],
});
h.elements = [container, text];
UI.clickTool("text");
mouse.clickAt(container.width / 2, container.height / 2);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).not.toBe(text.id);
expect(h.elements.length).toBe(3);
expect(h.state.editingElement?.id).toBe(h.elements[2].id);
});
});

View file

@ -23,6 +23,7 @@ const elementBase: Omit<ExcalidrawElement, "type"> = {
boundElements: null,
updated: 1,
link: null,
locked: false,
};
export const rectangleFixture: ExcalidrawElement = {

View file

@ -15,6 +15,7 @@ import path from "path";
import { getMimeType } from "../../data/blob";
import { newFreeDrawElement } from "../../element/newElement";
import { Point } from "../../types";
import { getSelectedElements } from "../../scene/selection";
const readFile = util.promisify(fs.readFile);
@ -30,10 +31,10 @@ export class API {
});
};
static getSelectedElements = (): ExcalidrawElement[] => {
return h.elements.filter(
(element) => h.state.selectedElementIds[element.id],
);
static getSelectedElements = (
includeBoundTextElement: boolean = false,
): ExcalidrawElement[] => {
return getSelectedElements(h.elements, h.state, includeBoundTextElement);
};
static getSelectedElement = (): ExcalidrawElement => {
@ -100,6 +101,7 @@ export class API {
? ExcalidrawTextElement["containerId"]
: never;
points?: T extends "arrow" | "line" ? readonly Point[] : never;
locked?: boolean;
}): T extends "arrow" | "line"
? ExcalidrawLinearElement
: T extends "freedraw"
@ -125,6 +127,7 @@ export class API {
roughness: rest.roughness ?? appState.currentItemRoughness,
opacity: rest.opacity ?? appState.currentItemOpacity,
boundElements: rest.boundElements ?? null,
locked: rest.locked ?? false,
};
switch (type) {
case "rectangle":

View file

@ -179,6 +179,14 @@ export class Pointer {
this.upAt();
}
rightClickAt(x: number, y: number) {
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: x,
clientY: y,
});
}
doubleClickAt(x: number, y: number) {
this.moveTo(x, y);
fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
@ -309,4 +317,10 @@ export class UI {
Keyboard.codePress(CODES.G);
});
}
static queryContextMenu = () => {
return GlobalTestState.renderResult.container.querySelector(
".context-menu",
);
};
}

View file

@ -93,7 +93,7 @@ exports[`exportToSvg with elements that have a link 1`] = `
exports[`exportToSvg with exportEmbedScene 1`] = `
"
<!-- svg-source:excalidraw -->
<!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1STU9cdTAwMDMhXHUwMDEwvfdXbPDapLtrv+ytWmNMjFx1MDAxZXpoovFAl9mFlFx1MDAwMlx1MDAwNbZcdTAwMWZp+t9cdTAwMDXaLrrx5lVcdTAwMGUk83hvZph5x06SIHtQgCZcdIJ9gTkjXHUwMDFh71DX41vQhknhnvJcdTAwMTBcdTAwMWJZ61wiMKm1atLrcelcdTAwMDRUXHUwMDFhe+ZcdTAwMDOHNVxia1x1MDAxY+PDxUlyXGa3e2HEq7ZcdTAwMGK9eZuWKyZIvinWo5fZ9Ok9SFx1MDAwM2nvOP2s38RcdTAwMDdf+HbUxDtGLHVYlqZcckaBVdS2QCwq7tuMiLFaruBBcql9IzdpOLH0XHUwMDEyXHUwMDE3q0rLWpDIyVx1MDAwNlx1MDAxOC/LyClcdTAwMTnnc3vg51x1MDAwMeCC1lx1MDAxYVCrwuLaYlx1MDAwYm90RrpcdTAwMDFHlStZUVx1MDAwMcb80EiFXHUwMDBiZlx1MDAwZq1f+f7UM1x00/1s56dYq0tcdTAwMWVkfPCtM1x1MDAwMFx1MDAxMlL1s+FgdJeOm5e43yxP2+irXHUwMDE0YddZNlx1MDAxZadpP1x1MDAxZlxyXHUwMDFiXHUwMDA2MzO3alx1MDAxYtKWmFx1MDAxYohz9CN8jDZcdTAwMTA1581jrVxiPoviVzlcdTAwMTOrNu9qR8LwWlxuglx1MDAwMn7q/jvq31F/dFx1MDAxNHDOlIGLo9xcdTAwMWR+jbBSc+tcdTAwMTI5ytlfaMtgd//LXHUwMDA2y3C8PvjRb1x1MDAxMHxXx1Pn9Fx1MDAwNbeWWs0ifQ==<!-- payload-end -->
<!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1SPW/CMFx1MDAxMN35XHUwMDE1kbtcIpGk4aNstFRVpapcdTAwMWRcdTAwMTiQWnUw8YVYMbaxXHUwMDFkPoT477VccsRtxNpcclx1MDAwZpbu+b278907dKJcYpm9XHUwMDA0NI5cdTAwMTDscswoUXiLulx1MDAwZd+A0lRw+5T6WIta5Z5ZXHUwMDFhI8e9XHUwMDFlXHUwMDEzVlBcbm1OfGCwXHUwMDAybrRlfNk4ilx1MDAwZf62L5Q41Wau1lx1MDAxZpOiopyk63w1fJtOXj691JN2lpMlWVx1MDAxM+9d4fthXHUwMDEzbykxpcWSOG6wXHUwMDEy6LI0LVx1MDAxMPMlc21cdTAwMDZEXHUwMDFiJSp4XHUwMDEyTCjXyF3sTyi9wHm1VKLmJHCSPsaLXCJwXG7K2Mzs2WlcdTAwMDA4L2tcdTAwMDWoVWF+abGFNzot7ICDypZcXJZcdTAwMWO0/qNcdTAwMTFcdTAwMTLn1Oxbv3L9yVfip/vdzl9iJc95kHbBr85cdTAwMDCIT5Ulg/7wIVx1MDAxZTUvYb9JXHUwMDFht9F3wf2uk2Q0iuMsXHUwMDFkXHUwMDBlXHUwMDFhXHUwMDA21VO7auPTXHUwMDE2mGlcYnN0I3xcdTAwMGU24DVjzWMtXHQ+icJXXHUwMDE55VWbZ11VXcl9cSmheCU4QVx1MDAxZT92b0a7XHUwMDE57X+MXHUwMDA2jFGp4Ww0e/thICzlzNj8lnKyXHUwMDFk2lDYPl5ZbOGP03ubusWCa/Zw7Fx1MDAxY39cdTAwMDCLqmbvIn0=<!-- payload-end -->
<defs>
<style>
@font-face {