Electron App (#224)
* add electron wrapper * add to workspaces * fixes electron setup * Fix package for dev * build out electron app communication * Update README.md
This commit is contained in:
parent
0d20994de9
commit
7c980ebb19
40 changed files with 4120 additions and 96 deletions
21
electron/LICENSE.md
Normal file
21
electron/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Stephen Ruiz Ltd
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
11
electron/README.md
Normal file
11
electron/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# @tldraw/tldraw-electron
|
||||||
|
|
||||||
|
An electron wrapper for TLDraw. Not yet published.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
From the root of the repository, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start:vscode
|
||||||
|
```
|
11
electron/electron-esbuild.config.yaml
Normal file
11
electron/electron-esbuild.config.yaml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
mainConfig:
|
||||||
|
type: esbuild
|
||||||
|
path: esbuild.main.config.ts
|
||||||
|
src: src/main/main.ts
|
||||||
|
output: dist/main
|
||||||
|
rendererConfig:
|
||||||
|
type: esbuild
|
||||||
|
path: esbuild.renderer.config.ts
|
||||||
|
html: src/renderer/index.html
|
||||||
|
src: src/renderer/index.tsx
|
||||||
|
output: dist/renderer
|
12
electron/esbuild.main.config.ts
Normal file
12
electron/esbuild.main.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { BuildOptions } from 'esbuild'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const config: BuildOptions = {
|
||||||
|
platform: 'node',
|
||||||
|
entryPoints: [path.resolve('src/main/main.ts'), path.resolve('src/main/preload.ts')],
|
||||||
|
bundle: true,
|
||||||
|
target: 'node16.5.0', // electron version target
|
||||||
|
sourcemap: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
12
electron/esbuild.renderer.config.ts
Normal file
12
electron/esbuild.renderer.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { BuildOptions } from 'esbuild'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const config: BuildOptions = {
|
||||||
|
platform: 'browser',
|
||||||
|
entryPoints: [path.resolve('src/renderer/index.tsx')],
|
||||||
|
bundle: true,
|
||||||
|
target: 'chrome94', // electron version target
|
||||||
|
sourcemap: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
78
electron/package.json
Normal file
78
electron/package.json
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"name": "@tldraw/electron",
|
||||||
|
"version": "0.1.4",
|
||||||
|
"private": true,
|
||||||
|
"description": "An electron app for tldraw.",
|
||||||
|
"author": "@steveruizok",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"react",
|
||||||
|
"typescript",
|
||||||
|
"esbuild"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"start:electron": "yarn dev",
|
||||||
|
"dev": "electron-esbuild dev",
|
||||||
|
"build": "electron-esbuild build",
|
||||||
|
"package": "electron-builder"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tldraw/tldraw": "^0.1.4",
|
||||||
|
"@types/node": "^14.14.35",
|
||||||
|
"@types/react": "^16.9.55",
|
||||||
|
"@types/react-dom": "^16.9.9",
|
||||||
|
"@types/react-router-dom": "^5.1.8",
|
||||||
|
"electron": "15.3.0",
|
||||||
|
"electron-builder": "^22.13.1",
|
||||||
|
"electron-esbuild": "^3.0.0",
|
||||||
|
"electron-util": "^0.17.2",
|
||||||
|
"esbuild": "^0.13.8",
|
||||||
|
"esbuild-serve": "^1.0.1",
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": "^16.8 || ^17.0",
|
||||||
|
"rimraf": "3.0.2",
|
||||||
|
"typescript": "4.2.3"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "io.comp.tldraw-electron",
|
||||||
|
"productName": "TLDraw",
|
||||||
|
"extraMetadata": {
|
||||||
|
"name": "TLDraw",
|
||||||
|
"main": "main.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"from": ".",
|
||||||
|
"filter": [
|
||||||
|
"package.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dist/main"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dist/renderer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"zip"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"target": [
|
||||||
|
"zip"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"zip"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "resources"
|
||||||
|
},
|
||||||
|
"publish": null
|
||||||
|
},
|
||||||
|
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
|
||||||
|
}
|
12
electron/resources/entitlements.mac.plist
Normal file
12
electron/resources/entitlements.mac.plist
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
BIN
electron/resources/icon.icns
Normal file
BIN
electron/resources/icon.icns
Normal file
Binary file not shown.
BIN
electron/resources/icon.ico
Normal file
BIN
electron/resources/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
20
electron/resources/notarize.js
Normal file
20
electron/resources/notarize.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
require('dotenv').config()
|
||||||
|
const { notarize } = require('electron-notarize')
|
||||||
|
|
||||||
|
exports.default = async function notarizing(context) {
|
||||||
|
const { electronPlatformName, appOutDir } = context
|
||||||
|
if (electronPlatformName !== 'darwin') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename
|
||||||
|
|
||||||
|
return await notarize({
|
||||||
|
appBundleId: 'com.tldraw.app',
|
||||||
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
|
appleId: process.env.APPLEID,
|
||||||
|
appleIdPassword: process.env.APPLEIDPASS,
|
||||||
|
})
|
||||||
|
}
|
114
electron/src/main/createMenu.ts
Normal file
114
electron/src/main/createMenu.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import { shell, app, Menu, MenuItemConstructorOptions } from 'electron'
|
||||||
|
import type { Message } from 'src/types'
|
||||||
|
|
||||||
|
export async function createMenu(send: (message: Message) => Promise<void>) {
|
||||||
|
const isMac = process.platform === 'darwin'
|
||||||
|
|
||||||
|
const template: MenuItemConstructorOptions[] = []
|
||||||
|
|
||||||
|
// About Menu (mac only)
|
||||||
|
if (isMac) {
|
||||||
|
template.push({
|
||||||
|
label: 'Hello world!',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'about' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'services' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'hide' },
|
||||||
|
{ role: 'hideOthers' },
|
||||||
|
{ role: 'unhide' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'quit' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// File Menu
|
||||||
|
template.push({
|
||||||
|
label: 'File',
|
||||||
|
submenu: [
|
||||||
|
{ label: 'New Project', click: () => send({ type: 'undo' }) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Open...', click: () => send({ type: 'redo' }) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Save', click: () => send({ type: 'redo' }) },
|
||||||
|
{ label: 'Save As...', click: () => send({ type: 'redo' }) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'quit' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Edit Menu
|
||||||
|
template.push({
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{ label: 'Undo', click: () => send({ type: 'undo' }), accelerator: 'CmdOrCtrl+Z' },
|
||||||
|
{ label: 'Redo', click: () => send({ type: 'redo' }), accelerator: 'CmdOrCtrl+Shift+Z' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Cut', click: () => send({ type: 'cut' }), accelerator: 'CmdOrCtrl+X' },
|
||||||
|
{ label: 'Copy', click: () => send({ type: 'copy' }), accelerator: 'CmdOrCtrl+C' },
|
||||||
|
{ label: 'Paste', click: () => send({ type: 'paste' }), accelerator: 'CmdOrCtrl+V' },
|
||||||
|
{ label: 'Delete', click: () => send({ type: 'delete' }), accelerator: 'Delete' },
|
||||||
|
{ label: 'Select All', click: () => send({ type: 'selectAll' }), accelerator: 'CmdOrCtrl+A' },
|
||||||
|
{ label: 'Select None', click: () => send({ type: 'selectNone' }) },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// View Menu
|
||||||
|
template.push({
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'reload' },
|
||||||
|
{ role: 'forceReload' },
|
||||||
|
{ role: 'toggleDevTools' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Actual Size',
|
||||||
|
click: () => send({ type: 'resetZoom' }),
|
||||||
|
},
|
||||||
|
{ label: 'Zoom In', click: () => send({ type: 'zoomIn' }) },
|
||||||
|
{ label: 'Zoom Out', click: () => send({ type: 'zoomOut' }) },
|
||||||
|
{ label: 'Zoom to Fit', click: () => send({ type: 'zoomToFit' }) },
|
||||||
|
{ label: 'Zoom to Selection', click: () => send({ type: 'zoomToSelection' }) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'togglefullscreen' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Window Menu
|
||||||
|
if (isMac) {
|
||||||
|
template.push({
|
||||||
|
label: 'Window',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'minimize' },
|
||||||
|
{ role: 'zoom' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'front' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'window' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
template.push({
|
||||||
|
label: 'Window',
|
||||||
|
submenu: [{ role: 'minimize' }, { role: 'zoom' }, { role: 'close' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
template.push({
|
||||||
|
role: 'help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Learn More',
|
||||||
|
click: async () => {
|
||||||
|
await shell.openExternal('https://electronjs.org')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(template)
|
||||||
|
|
||||||
|
Menu.setApplicationMenu(menu)
|
||||||
|
}
|
58
electron/src/main/createWindow.ts
Normal file
58
electron/src/main/createWindow.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import path from 'path'
|
||||||
|
import { format } from 'url'
|
||||||
|
import { BrowserWindow } from 'electron'
|
||||||
|
import { is } from 'electron-util'
|
||||||
|
|
||||||
|
export async function createWindow() {
|
||||||
|
let win: BrowserWindow | null = null
|
||||||
|
|
||||||
|
win = new BrowserWindow({
|
||||||
|
width: 720,
|
||||||
|
height: 450,
|
||||||
|
minHeight: 480,
|
||||||
|
minWidth: 600,
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
title: 'TLDraw',
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
devTools: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
},
|
||||||
|
frame: false,
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
win.setWindowButtonVisibility(false)
|
||||||
|
|
||||||
|
const isDev = is.development
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
win.loadURL('http://localhost:9080')
|
||||||
|
} else {
|
||||||
|
win.loadURL(path.join(__dirname, 'index.html'))
|
||||||
|
}
|
||||||
|
|
||||||
|
win.setPosition(0, 0, false)
|
||||||
|
win.setSize(700, 1200)
|
||||||
|
|
||||||
|
win.on('closed', () => {
|
||||||
|
win = null
|
||||||
|
})
|
||||||
|
|
||||||
|
win.webContents.on('devtools-opened', () => {
|
||||||
|
win!.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
win.on('ready-to-show', () => {
|
||||||
|
win!.show()
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
win!.webContents.openDevTools({ mode: 'bottom' })
|
||||||
|
} else {
|
||||||
|
win!.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return win
|
||||||
|
}
|
32
electron/src/main/main.ts
Normal file
32
electron/src/main/main.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import { app, BrowserWindow } from 'electron'
|
||||||
|
import { is } from 'electron-util'
|
||||||
|
import type { Message } from 'src/types'
|
||||||
|
import { createMenu } from './createMenu'
|
||||||
|
import { createWindow } from './createWindow'
|
||||||
|
import './preload'
|
||||||
|
|
||||||
|
let win: BrowserWindow | null = null
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
win = await createWindow()
|
||||||
|
|
||||||
|
async function send(message: Message) {
|
||||||
|
win!.webContents.send('projectMsg', message)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createMenu(send)
|
||||||
|
}
|
||||||
|
|
||||||
|
app
|
||||||
|
.on('ready', main)
|
||||||
|
.on('window-all-closed', () => {
|
||||||
|
if (!is.macos) {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('activate', () => {
|
||||||
|
if (win === null && app.isReady()) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
})
|
15
electron/src/main/preload.ts
Normal file
15
electron/src/main/preload.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
import type { Message, TLApi } from 'src/types'
|
||||||
|
|
||||||
|
const api: TLApi = {
|
||||||
|
send: (channel: string, data: Message) => {
|
||||||
|
ipcRenderer.send(channel, data)
|
||||||
|
},
|
||||||
|
on: (channel, cb) => {
|
||||||
|
ipcRenderer.on(channel, (event, message) => cb(message as Message))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
contextBridge?.exposeInMainWorld('TLApi', api)
|
||||||
|
|
||||||
|
export {}
|
85
electron/src/renderer/app.tsx
Normal file
85
electron/src/renderer/app.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||||
|
import type { IpcMainEvent, IpcMain, IpcRenderer } from 'electron'
|
||||||
|
import type { Message, TLApi } from 'src/types'
|
||||||
|
|
||||||
|
export default function App(): JSX.Element {
|
||||||
|
const rTLDrawState = React.useRef<TLDrawState>()
|
||||||
|
|
||||||
|
// When the editor mounts, save the tlstate instance in a ref.
|
||||||
|
const handleMount = React.useCallback((tldr: TLDrawState) => {
|
||||||
|
rTLDrawState.current = tldr
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function handleEvent(message: Message) {
|
||||||
|
const tlstate = rTLDrawState.current
|
||||||
|
if (!tlstate) return
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case 'resetZoom': {
|
||||||
|
tlstate.resetZoom()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'zoomIn': {
|
||||||
|
tlstate.zoomIn()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'zoomOut': {
|
||||||
|
tlstate.zoomOut()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'zoomToFit': {
|
||||||
|
tlstate.zoomToFit()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'zoomToSelection': {
|
||||||
|
tlstate.zoomToSelection()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'undo': {
|
||||||
|
tlstate.undo()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'redo': {
|
||||||
|
tlstate.redo()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'cut': {
|
||||||
|
tlstate.cut()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'copy': {
|
||||||
|
tlstate.copy()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'paste': {
|
||||||
|
tlstate.paste()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
tlstate.delete()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'selectAll': {
|
||||||
|
tlstate.selectAll()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'selectNone': {
|
||||||
|
tlstate.selectNone()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { send, on } = (window as unknown as Window & { TLApi: TLApi })['TLApi']
|
||||||
|
|
||||||
|
on('projectMsg', handleEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tldraw">
|
||||||
|
<TLDraw id="electron" onMount={handleMount} autofocus showMenu={false} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
0
electron/src/renderer/decs.d.ts
vendored
Normal file
0
electron/src/renderer/decs.d.ts
vendored
Normal file
13
electron/src/renderer/index.html
Normal file
13
electron/src/renderer/index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="stylesheet" href="index.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<script type="module" src="index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
11
electron/src/renderer/index.tsx
Normal file
11
electron/src/renderer/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import App from './app'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
)
|
20
electron/src/renderer/styles.css
Normal file
20
electron/src/renderer/styles.css
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
html,
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tldraw {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
19
electron/src/types.ts
Normal file
19
electron/src/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export type Message =
|
||||||
|
| { type: 'zoomIn' }
|
||||||
|
| { type: 'zoomOut' }
|
||||||
|
| { type: 'resetZoom' }
|
||||||
|
| { type: 'zoomToFit' }
|
||||||
|
| { type: 'zoomToSelection' }
|
||||||
|
| { type: 'undo' }
|
||||||
|
| { type: 'redo' }
|
||||||
|
| { type: 'cut' }
|
||||||
|
| { type: 'copy' }
|
||||||
|
| { type: 'paste' }
|
||||||
|
| { type: 'delete' }
|
||||||
|
| { type: 'selectAll' }
|
||||||
|
| { type: 'selectNone' }
|
||||||
|
|
||||||
|
export type TLApi = {
|
||||||
|
send: (channel: string, data: Message) => void
|
||||||
|
on: (channel: string, cb: (message: Message) => void) => void
|
||||||
|
}
|
24
electron/tsconfig.json
Normal file
24
electron/tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist", "docs"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/types",
|
||||||
|
"rootDir": ".",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"allowJs": false,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"paths": {
|
||||||
|
"@tldraw/tldraw": ["../packages/tldraw"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../packages/tldraw"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typedocOptions": {
|
||||||
|
"entryPoints": ["src/index.ts"],
|
||||||
|
"out": "docs"
|
||||||
|
}
|
||||||
|
}
|
1892
electron/yarn.lock
Normal file
1892
electron/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -9,6 +9,7 @@
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
"electron",
|
||||||
"vscode/editor",
|
"vscode/editor",
|
||||||
"packages/tldraw",
|
"packages/tldraw",
|
||||||
"example",
|
"example",
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watchAll",
|
"test:watch": "jest --watchAll",
|
||||||
"lerna": "lerna",
|
"lerna": "lerna",
|
||||||
|
"start:electron": "lerna run start:electron --stream --parallel",
|
||||||
"start:vscode": "lerna run start:vscode --stream --parallel",
|
"start:vscode": "lerna run start:vscode --stream --parallel",
|
||||||
"start": "lerna run start --stream --parallel",
|
"start": "lerna run start --stream --parallel",
|
||||||
"start:www": "yarn build:packages && lerna run start --parallel & cd www && yarn dev",
|
"start:www": "yarn build:packages && lerna run start --parallel & cd www && yarn dev",
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"module": "./dist/esm/index.js",
|
"module": "./dist/esm/index.js",
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start:electron": "yarn start",
|
||||||
"start:vscode": "yarn start",
|
"start:vscode": "yarn start",
|
||||||
"start": "node scripts/dev & yarn types:dev",
|
"start": "node scripts/dev & yarn types:dev",
|
||||||
"build": "node scripts/build && yarn types:build && node scripts/copy-readme",
|
"build": "node scripts/build && yarn types:build && node scripts/copy-readme",
|
||||||
|
|
|
@ -24,6 +24,10 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
||||||
callbacks.onSignOut?.(tlstate)
|
callbacks.onSignOut?.(tlstate)
|
||||||
}, [tlstate])
|
}, [tlstate])
|
||||||
|
|
||||||
|
const handleCut = React.useCallback(() => {
|
||||||
|
tlstate.cut()
|
||||||
|
}, [tlstate])
|
||||||
|
|
||||||
const handleCopy = React.useCallback(() => {
|
const handleCopy = React.useCallback(() => {
|
||||||
tlstate.copy()
|
tlstate.copy()
|
||||||
}, [tlstate])
|
}, [tlstate])
|
||||||
|
@ -44,8 +48,8 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
||||||
tlstate.selectAll()
|
tlstate.selectAll()
|
||||||
}, [tlstate])
|
}, [tlstate])
|
||||||
|
|
||||||
const handleDeselectAll = React.useCallback(() => {
|
const handleselectNone = React.useCallback(() => {
|
||||||
tlstate.deselectAll()
|
tlstate.selectNone()
|
||||||
}, [tlstate])
|
}, [tlstate])
|
||||||
|
|
||||||
const showFileMenu =
|
const showFileMenu =
|
||||||
|
@ -96,6 +100,9 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
||||||
Redo
|
Redo
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMDivider dir="ltr" />
|
<DMDivider dir="ltr" />
|
||||||
|
<DMItem onSelect={handleCut} kbd="#X">
|
||||||
|
Cut
|
||||||
|
</DMItem>
|
||||||
<DMItem onSelect={handleCopy} kbd="#C">
|
<DMItem onSelect={handleCopy} kbd="#C">
|
||||||
Copy
|
Copy
|
||||||
</DMItem>
|
</DMItem>
|
||||||
|
@ -111,7 +118,7 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
||||||
<DMItem onSelect={handleSelectAll} kbd="#A">
|
<DMItem onSelect={handleSelectAll} kbd="#A">
|
||||||
Select All
|
Select All
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={handleDeselectAll}>Select None</DMItem>
|
<DMItem onSelect={handleselectNone}>Select None</DMItem>
|
||||||
</DMSubMenu>
|
</DMSubMenu>
|
||||||
<DMDivider dir="ltr" />
|
<DMDivider dir="ltr" />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -24,7 +24,7 @@ export function ZoomMenu() {
|
||||||
<DMItem onSelect={tlstate.zoomOut} kbd="#−">
|
<DMItem onSelect={tlstate.zoomOut} kbd="#−">
|
||||||
Zoom Out
|
Zoom Out
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={tlstate.zoomToActual} kbd="⇧0">
|
<DMItem onSelect={tlstate.resetZoom} kbd="⇧0">
|
||||||
To 100%
|
To 100%
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={tlstate.zoomToFit} kbd="⇧1">
|
<DMItem onSelect={tlstate.zoomToFit} kbd="⇧1">
|
||||||
|
|
|
@ -249,7 +249,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+0',
|
'shift+0',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) tlstate.zoomToActual()
|
if (canHandleEvent()) tlstate.resetZoom()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[tlstate]
|
[tlstate]
|
||||||
|
@ -398,7 +398,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
[tlstate]
|
[tlstate]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Copy & Paste
|
// Copy, Cut & Paste
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+c,ctrl+c',
|
'command+c,ctrl+c',
|
||||||
|
@ -409,6 +409,15 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
[tlstate]
|
[tlstate]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'command+x,ctrl+x',
|
||||||
|
() => {
|
||||||
|
if (canHandleEvent()) tlstate.cut()
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[tlstate]
|
||||||
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+v,ctrl+v',
|
'command+v,ctrl+v',
|
||||||
() => {
|
() => {
|
||||||
|
|
|
@ -11,7 +11,7 @@ describe('TLDrawState', () => {
|
||||||
|
|
||||||
describe('When copying and pasting...', () => {
|
describe('When copying and pasting...', () => {
|
||||||
it('copies a shape', () => {
|
it('copies a shape', () => {
|
||||||
tlstate.loadDocument(mockDocument).deselectAll().copy(['rect1'])
|
tlstate.loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pastes a shape', () => {
|
it('pastes a shape', () => {
|
||||||
|
@ -19,7 +19,7 @@ describe('TLDrawState', () => {
|
||||||
|
|
||||||
const prevCount = Object.keys(tlstate.page.shapes).length
|
const prevCount = Object.keys(tlstate.page.shapes).length
|
||||||
|
|
||||||
tlstate.deselectAll().copy(['rect1']).paste()
|
tlstate.selectNone().copy(['rect1']).paste()
|
||||||
|
|
||||||
expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount + 1)
|
expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount + 1)
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ describe('TLDrawState', () => {
|
||||||
it('pastes a shape to a new page', () => {
|
it('pastes a shape to a new page', () => {
|
||||||
tlstate.loadDocument(mockDocument)
|
tlstate.loadDocument(mockDocument)
|
||||||
|
|
||||||
tlstate.deselectAll().copy(['rect1']).createPage().paste()
|
tlstate.selectNone().copy(['rect1']).createPage().paste()
|
||||||
|
|
||||||
expect(Object.keys(tlstate.page.shapes).length).toBe(1)
|
expect(Object.keys(tlstate.page.shapes).length).toBe(1)
|
||||||
|
|
||||||
|
@ -135,14 +135,14 @@ describe('TLDrawState', () => {
|
||||||
|
|
||||||
describe('Selection', () => {
|
describe('Selection', () => {
|
||||||
it('selects a shape', () => {
|
it('selects a shape', () => {
|
||||||
tlstate.loadDocument(mockDocument).deselectAll()
|
tlstate.loadDocument(mockDocument).selectNone()
|
||||||
tlu.clickShape('rect1')
|
tlu.clickShape('rect1')
|
||||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||||
expect(tlstate.appState.status).toBe('idle')
|
expect(tlstate.appState.status).toBe('idle')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('selects and deselects a shape', () => {
|
it('selects and deselects a shape', () => {
|
||||||
tlstate.loadDocument(mockDocument).deselectAll()
|
tlstate.loadDocument(mockDocument).selectNone()
|
||||||
tlu.clickShape('rect1')
|
tlu.clickShape('rect1')
|
||||||
tlu.clickCanvas()
|
tlu.clickCanvas()
|
||||||
expect(tlstate.selectedIds).toStrictEqual([])
|
expect(tlstate.selectedIds).toStrictEqual([])
|
||||||
|
@ -150,7 +150,7 @@ describe('TLDrawState', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('selects multiple shapes', () => {
|
it('selects multiple shapes', () => {
|
||||||
tlstate.loadDocument(mockDocument).deselectAll()
|
tlstate.loadDocument(mockDocument).selectNone()
|
||||||
tlu.clickShape('rect1')
|
tlu.clickShape('rect1')
|
||||||
tlu.clickShape('rect2', { shiftKey: true })
|
tlu.clickShape('rect2', { shiftKey: true })
|
||||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||||
|
@ -158,7 +158,7 @@ describe('TLDrawState', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shift-selects to deselect shapes', () => {
|
it('shift-selects to deselect shapes', () => {
|
||||||
tlstate.loadDocument(mockDocument).deselectAll()
|
tlstate.loadDocument(mockDocument).selectNone()
|
||||||
tlu.clickShape('rect1')
|
tlu.clickShape('rect1')
|
||||||
tlu.clickShape('rect2', { shiftKey: true })
|
tlu.clickShape('rect2', { shiftKey: true })
|
||||||
tlu.clickShape('rect2', { shiftKey: true })
|
tlu.clickShape('rect2', { shiftKey: true })
|
||||||
|
@ -167,7 +167,7 @@ describe('TLDrawState', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('clears selection when clicking bounds', () => {
|
it('clears selection when clicking bounds', () => {
|
||||||
tlstate.loadDocument(mockDocument).deselectAll()
|
tlstate.loadDocument(mockDocument).selectNone()
|
||||||
tlstate.startSession(SessionType.Brush, [-10, -10])
|
tlstate.startSession(SessionType.Brush, [-10, -10])
|
||||||
tlstate.updateSession([110, 110])
|
tlstate.updateSession([110, 110])
|
||||||
tlstate.completeSession()
|
tlstate.completeSession()
|
||||||
|
@ -187,7 +187,7 @@ describe('TLDrawState', () => {
|
||||||
// })
|
// })
|
||||||
|
|
||||||
it('does not select on meta-click', () => {
|
it('does not select on meta-click', () => {
|
||||||
tlstate.loadDocument(mockDocument).deselectAll()
|
tlstate.loadDocument(mockDocument).selectNone()
|
||||||
tlu.clickShape('rect1', { ctrlKey: true })
|
tlu.clickShape('rect1', { ctrlKey: true })
|
||||||
expect(tlstate.selectedIds).toStrictEqual([])
|
expect(tlstate.selectedIds).toStrictEqual([])
|
||||||
expect(tlstate.appState.status).toBe('idle')
|
expect(tlstate.appState.status).toBe('idle')
|
||||||
|
@ -214,7 +214,7 @@ describe('TLDrawState', () => {
|
||||||
// Single click on a selected shape to select just that shape
|
// Single click on a selected shape to select just that shape
|
||||||
|
|
||||||
it('single-selects shape in selection on click', () => {
|
it('single-selects shape in selection on click', () => {
|
||||||
tlstate.deselectAll()
|
tlstate.selectNone()
|
||||||
tlu.clickShape('rect1')
|
tlu.clickShape('rect1')
|
||||||
tlu.clickShape('rect2', { shiftKey: true })
|
tlu.clickShape('rect2', { shiftKey: true })
|
||||||
tlu.clickShape('rect2')
|
tlu.clickShape('rect2')
|
||||||
|
@ -223,7 +223,7 @@ describe('TLDrawState', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('single-selects shape in selection on pointerup only', () => {
|
it('single-selects shape in selection on pointerup only', () => {
|
||||||
tlstate.deselectAll()
|
tlstate.selectNone()
|
||||||
tlu.clickShape('rect1')
|
tlu.clickShape('rect1')
|
||||||
tlu.clickShape('rect2', { shiftKey: true })
|
tlu.clickShape('rect2', { shiftKey: true })
|
||||||
tlu.pointShape('rect2')
|
tlu.pointShape('rect2')
|
||||||
|
@ -234,7 +234,7 @@ describe('TLDrawState', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// it('selects shapes if shift key is lifted before pointerup', () => {
|
// it('selects shapes if shift key is lifted before pointerup', () => {
|
||||||
// tlstate.deselectAll()
|
// tlstate.selectNone()
|
||||||
// tlu.clickShape('rect1')
|
// tlu.clickShape('rect1')
|
||||||
// tlu.pointShape('rect2', { shiftKey: true })
|
// tlu.pointShape('rect2', { shiftKey: true })
|
||||||
// expect(tlstate.appState.status).toBe('pointingBounds')
|
// expect(tlstate.appState.status).toBe('pointingBounds')
|
||||||
|
@ -390,7 +390,7 @@ describe('TLDrawState', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.group(['rect1', 'rect2'], 'groupA')
|
.group(['rect1', 'rect2'], 'groupA')
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
|
|
||||||
const tlu = new TLDrawStateUtils(tlstate)
|
const tlu = new TLDrawStateUtils(tlstate)
|
||||||
|
|
||||||
|
@ -454,7 +454,7 @@ describe('TLDrawState', () => {
|
||||||
|
|
||||||
expect(tlstate.getShape('groupA').childIndex).toBe(2)
|
expect(tlstate.getShape('groupA').childIndex).toBe(2)
|
||||||
|
|
||||||
tlstate.deselectAll()
|
tlstate.selectNone()
|
||||||
tlstate.selectTool(TLDrawShapeType.Rectangle)
|
tlstate.selectTool(TLDrawShapeType.Rectangle)
|
||||||
|
|
||||||
const prevB = tlstate.shapes.map((shape) => shape.id)
|
const prevB = tlstate.shapes.map((shape) => shape.id)
|
||||||
|
|
|
@ -685,7 +685,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
if (!document.pages[pageId]) {
|
if (!document.pages[pageId]) {
|
||||||
if (pageId === this.appState.currentPageId) {
|
if (pageId === this.appState.currentPageId) {
|
||||||
this.cancelSession()
|
this.cancelSession()
|
||||||
this.deselectAll()
|
this.selectNone()
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPageStates[pageId] = undefined as unknown as TLPageState
|
currentPageStates[pageId] = undefined as unknown as TLPageState
|
||||||
|
@ -812,7 +812,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
* @param document The document to load
|
* @param document The document to load
|
||||||
*/
|
*/
|
||||||
loadDocument = (document: TLDrawDocument): this => {
|
loadDocument = (document: TLDrawDocument): this => {
|
||||||
this.deselectAll()
|
this.selectNone()
|
||||||
this.resetHistory()
|
this.resetHistory()
|
||||||
this.clearSelectHistory()
|
this.clearSelectHistory()
|
||||||
this.session = undefined
|
this.session = undefined
|
||||||
|
@ -1165,6 +1165,16 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cut (copy and delete) one or more shapes to the clipboard.
|
||||||
|
* @param ids The ids of the shapes to cut.
|
||||||
|
*/
|
||||||
|
cut = (ids = this.selectedIds): this => {
|
||||||
|
this.copy(ids)
|
||||||
|
this.delete(ids)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paste shapes (or text) from clipboard to a certain point.
|
* Paste shapes (or text) from clipboard to a certain point.
|
||||||
* @param point
|
* @param point
|
||||||
|
@ -1579,7 +1589,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
/**
|
/**
|
||||||
* Zoom the camera to 100%.
|
* Zoom the camera to 100%.
|
||||||
*/
|
*/
|
||||||
zoomToActual = (): this => {
|
resetZoom = (): this => {
|
||||||
return this.zoomTo(1)
|
return this.zoomTo(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1714,7 +1724,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
/**
|
/**
|
||||||
* Deselect any selected shapes.
|
* Deselect any selected shapes.
|
||||||
*/
|
*/
|
||||||
deselectAll = (): this => {
|
selectNone = (): this => {
|
||||||
this.setSelectedIds([])
|
this.setSelectedIds([])
|
||||||
this.addToSelectHistory(this.selectedIds)
|
this.addToSelectHistory(this.selectedIds)
|
||||||
return this
|
return this
|
||||||
|
|
|
@ -73,7 +73,7 @@ describe('Delete command', () => {
|
||||||
expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined)
|
expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined)
|
||||||
|
|
||||||
tlstate
|
tlstate
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.createShapes({
|
.createShapes({
|
||||||
id: 'arrow1',
|
id: 'arrow1',
|
||||||
type: TLDrawShapeType.Arrow,
|
type: TLDrawShapeType.Arrow,
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe('Group command', () => {
|
||||||
|
|
||||||
describe('when less than two shapes are selected', () => {
|
describe('when less than two shapes are selected', () => {
|
||||||
it('does nothing', () => {
|
it('does nothing', () => {
|
||||||
tlstate.deselectAll()
|
tlstate.selectNone()
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const stackLength = tlstate.stack.length
|
const stackLength = tlstate.stack.length
|
||||||
|
|
|
@ -47,12 +47,12 @@ describe('when running the command', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.rotate()
|
.rotate()
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.undo()
|
.undo()
|
||||||
|
|
||||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||||
|
|
||||||
tlstate.deselectAll().redo()
|
tlstate.selectNone().redo()
|
||||||
|
|
||||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||||
})
|
})
|
||||||
|
|
|
@ -72,12 +72,12 @@ describe('when running the command', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.stretch(StretchType.Horizontal)
|
.stretch(StretchType.Horizontal)
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.undo()
|
.undo()
|
||||||
|
|
||||||
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
|
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
|
||||||
|
|
||||||
tlstate.deselectAll().redo()
|
tlstate.selectNone().redo()
|
||||||
|
|
||||||
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
|
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
|
||||||
})
|
})
|
||||||
|
|
|
@ -82,12 +82,12 @@ describe('when running the command', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.style({ size: SizeStyle.Small })
|
.style({ size: SizeStyle.Small })
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.undo()
|
.undo()
|
||||||
|
|
||||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||||
|
|
||||||
tlstate.deselectAll().redo()
|
tlstate.selectNone().redo()
|
||||||
|
|
||||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||||
})
|
})
|
||||||
|
|
|
@ -67,12 +67,12 @@ describe('when running the command', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.toggleHidden()
|
.toggleHidden()
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.undo()
|
.undo()
|
||||||
|
|
||||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||||
|
|
||||||
tlstate.deselectAll().redo()
|
tlstate.selectNone().redo()
|
||||||
|
|
||||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,7 +6,7 @@ describe('Brush session', () => {
|
||||||
it('begins, updateSession', () => {
|
it('begins, updateSession', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.startSession(SessionType.Brush, [-10, -10])
|
.startSession(SessionType.Brush, [-10, -10])
|
||||||
.updateSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
@ -17,7 +17,7 @@ describe('Brush session', () => {
|
||||||
it('selects multiple shapes', () => {
|
it('selects multiple shapes', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.startSession(SessionType.Brush, [-10, -10])
|
.startSession(SessionType.Brush, [-10, -10])
|
||||||
.updateSession([110, 110])
|
.updateSession([110, 110])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
@ -27,7 +27,7 @@ describe('Brush session', () => {
|
||||||
it('does not de-select original shapes', () => {
|
it('does not de-select original shapes', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startSession(SessionType.Brush, [300, 300])
|
.startSession(SessionType.Brush, [300, 300])
|
||||||
.updateSession([301, 301])
|
.updateSession([301, 301])
|
||||||
|
@ -38,9 +38,9 @@ describe('Brush session', () => {
|
||||||
// it('does not select hidden shapes', () => {
|
// it('does not select hidden shapes', () => {
|
||||||
// const tlstate = new TLDrawState()
|
// const tlstate = new TLDrawState()
|
||||||
// .loadDocument(mockDocument)
|
// .loadDocument(mockDocument)
|
||||||
// .deselectAll()
|
// .selectNone()
|
||||||
// .toggleHidden(['rect1'])
|
// .toggleHidden(['rect1'])
|
||||||
// .deselectAll()
|
// .selectNone()
|
||||||
// .startSession(SessionType.Brush, [-10, -10])
|
// .startSession(SessionType.Brush, [-10, -10])
|
||||||
// .updateSession([10, 10])
|
// .updateSession([10, 10])
|
||||||
// .completeSession()
|
// .completeSession()
|
||||||
|
@ -49,9 +49,9 @@ describe('Brush session', () => {
|
||||||
it('when command is held, require the entire shape to be selected', () => {
|
it('when command is held, require the entire shape to be selected', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.deselectAll()
|
.selectNone()
|
||||||
.startSession(SessionType.Brush, [-10, -10])
|
.startSession(SessionType.Brush, [-10, -10])
|
||||||
.updateSession([10, 10], false, false, true)
|
.updateSession([10, 10], false, false, true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
|
@ -50,7 +50,7 @@ export class DrawUtil extends TLDrawShapeUtil<T, E> {
|
||||||
return style.dash === DashStyle.Draw
|
return style.dash === DashStyle.Draw
|
||||||
? getDrawStrokePathData(shape)
|
? getDrawStrokePathData(shape)
|
||||||
: getSolidStrokePathData(shape)
|
: getSolidStrokePathData(shape)
|
||||||
}, [points, style.size, style.dash, isComplete, false])
|
}, [points, style.size, style.dash, isComplete])
|
||||||
|
|
||||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ describe('When double clicking link controls', () => {
|
||||||
.startSession(SessionType.Arrow, [200, 200], 'end')
|
.startSession(SessionType.Arrow, [200, 200], 'end')
|
||||||
.updateSession([250, 50])
|
.updateSession([250, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
.deselectAll().document
|
.selectNone().document
|
||||||
|
|
||||||
it('moves all linked shapes when center is dragged', () => {
|
it('moves all linked shapes when center is dragged', () => {
|
||||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||||
|
|
|
@ -63,8 +63,8 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
this.state.select(...this.state.selectedIds.filter((oid) => oid !== shape.parentId), id)
|
this.state.select(...this.state.selectedIds.filter((oid) => oid !== shape.parentId), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private deselectAll() {
|
private selectNone() {
|
||||||
this.state.deselectAll()
|
this.state.selectNone()
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnter = () => {
|
onEnter = () => {
|
||||||
|
@ -172,7 +172,7 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
/* ----------------- Event Handlers ----------------- */
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
onCancel = () => {
|
onCancel = () => {
|
||||||
this.deselectAll()
|
this.selectNone()
|
||||||
this.state.cancelSession()
|
this.state.cancelSession()
|
||||||
this.setStatus(Status.Idle)
|
this.setStatus(Status.Idle)
|
||||||
}
|
}
|
||||||
|
@ -379,7 +379,7 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
if (info.target === 'bounds') {
|
if (info.target === 'bounds') {
|
||||||
// If we just clicked the selecting bounds's background,
|
// If we just clicked the selecting bounds's background,
|
||||||
// clear the selection
|
// clear the selection
|
||||||
this.deselectAll()
|
this.selectNone()
|
||||||
} else if (this.state.isSelected(info.target)) {
|
} else if (this.state.isSelected(info.target)) {
|
||||||
// If we're holding shift...
|
// If we're holding shift...
|
||||||
if (info.shiftKey) {
|
if (info.shiftKey) {
|
||||||
|
@ -438,7 +438,7 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deselectAll()
|
this.selectNone()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setStatus(Status.PointingCanvas)
|
this.setStatus(Status.PointingCanvas)
|
||||||
|
@ -494,7 +494,7 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
|
|
||||||
if (info.metaKey) {
|
if (info.metaKey) {
|
||||||
if (!info.shiftKey) {
|
if (!info.shiftKey) {
|
||||||
this.deselectAll()
|
this.selectNone()
|
||||||
}
|
}
|
||||||
|
|
||||||
const point = this.state.getPagePoint(info.point)
|
const point = this.state.getPagePoint(info.point)
|
||||||
|
@ -595,7 +595,7 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
onPointBounds: TLBoundsEventHandler = (info) => {
|
onPointBounds: TLBoundsEventHandler = (info) => {
|
||||||
if (info.metaKey) {
|
if (info.metaKey) {
|
||||||
if (!info.shiftKey) {
|
if (!info.shiftKey) {
|
||||||
this.deselectAll()
|
this.selectNone()
|
||||||
}
|
}
|
||||||
|
|
||||||
const point = this.state.getPagePoint(info.point)
|
const point = this.state.getPagePoint(info.point)
|
||||||
|
|
Loading…
Reference in a new issue