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",
|
||||
"workspaces": [
|
||||
"electron",
|
||||
"vscode/editor",
|
||||
"packages/tldraw",
|
||||
"example",
|
||||
|
@ -18,6 +19,7 @@
|
|||
"test": "jest",
|
||||
"test:watch": "jest --watchAll",
|
||||
"lerna": "lerna",
|
||||
"start:electron": "lerna run start:electron --stream --parallel",
|
||||
"start:vscode": "lerna run start:vscode --stream --parallel",
|
||||
"start": "lerna run start --stream --parallel",
|
||||
"start:www": "yarn build:packages && lerna run start --parallel & cd www && yarn dev",
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"scripts": {
|
||||
"start:electron": "yarn start",
|
||||
"start:vscode": "yarn start",
|
||||
"start": "node scripts/dev & yarn types:dev",
|
||||
"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)
|
||||
}, [tlstate])
|
||||
|
||||
const handleCut = React.useCallback(() => {
|
||||
tlstate.cut()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
tlstate.copy()
|
||||
}, [tlstate])
|
||||
|
@ -44,8 +48,8 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
|||
tlstate.selectAll()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDeselectAll = React.useCallback(() => {
|
||||
tlstate.deselectAll()
|
||||
const handleselectNone = React.useCallback(() => {
|
||||
tlstate.selectNone()
|
||||
}, [tlstate])
|
||||
|
||||
const showFileMenu =
|
||||
|
@ -96,6 +100,9 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
|||
Redo
|
||||
</DMItem>
|
||||
<DMDivider dir="ltr" />
|
||||
<DMItem onSelect={handleCut} kbd="#X">
|
||||
Cut
|
||||
</DMItem>
|
||||
<DMItem onSelect={handleCopy} kbd="#C">
|
||||
Copy
|
||||
</DMItem>
|
||||
|
@ -111,7 +118,7 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
|||
<DMItem onSelect={handleSelectAll} kbd="#A">
|
||||
Select All
|
||||
</DMItem>
|
||||
<DMItem onSelect={handleDeselectAll}>Select None</DMItem>
|
||||
<DMItem onSelect={handleselectNone}>Select None</DMItem>
|
||||
</DMSubMenu>
|
||||
<DMDivider dir="ltr" />
|
||||
</>
|
||||
|
|
|
@ -24,7 +24,7 @@ export function ZoomMenu() {
|
|||
<DMItem onSelect={tlstate.zoomOut} kbd="#−">
|
||||
Zoom Out
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomToActual} kbd="⇧0">
|
||||
<DMItem onSelect={tlstate.resetZoom} kbd="⇧0">
|
||||
To 100%
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomToFit} kbd="⇧1">
|
||||
|
|
|
@ -249,7 +249,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'shift+0',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.zoomToActual()
|
||||
if (canHandleEvent()) tlstate.resetZoom()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
|
@ -398,7 +398,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
[tlstate]
|
||||
)
|
||||
|
||||
// Copy & Paste
|
||||
// Copy, Cut & Paste
|
||||
|
||||
useHotkeys(
|
||||
'command+c,ctrl+c',
|
||||
|
@ -409,6 +409,15 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
[tlstate]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+x,ctrl+x',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.cut()
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+v,ctrl+v',
|
||||
() => {
|
||||
|
|
|
@ -11,7 +11,7 @@ describe('TLDrawState', () => {
|
|||
|
||||
describe('When copying and pasting...', () => {
|
||||
it('copies a shape', () => {
|
||||
tlstate.loadDocument(mockDocument).deselectAll().copy(['rect1'])
|
||||
tlstate.loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||
})
|
||||
|
||||
it('pastes a shape', () => {
|
||||
|
@ -19,7 +19,7 @@ describe('TLDrawState', () => {
|
|||
|
||||
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)
|
||||
|
||||
|
@ -35,7 +35,7 @@ describe('TLDrawState', () => {
|
|||
it('pastes a shape to a new page', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
|
||||
tlstate.deselectAll().copy(['rect1']).createPage().paste()
|
||||
tlstate.selectNone().copy(['rect1']).createPage().paste()
|
||||
|
||||
expect(Object.keys(tlstate.page.shapes).length).toBe(1)
|
||||
|
||||
|
@ -135,14 +135,14 @@ describe('TLDrawState', () => {
|
|||
|
||||
describe('Selection', () => {
|
||||
it('selects a shape', () => {
|
||||
tlstate.loadDocument(mockDocument).deselectAll()
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('selects and deselects a shape', () => {
|
||||
tlstate.loadDocument(mockDocument).deselectAll()
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickCanvas()
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
|
@ -150,7 +150,7 @@ describe('TLDrawState', () => {
|
|||
})
|
||||
|
||||
it('selects multiple shapes', () => {
|
||||
tlstate.loadDocument(mockDocument).deselectAll()
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
|
@ -158,7 +158,7 @@ describe('TLDrawState', () => {
|
|||
})
|
||||
|
||||
it('shift-selects to deselect shapes', () => {
|
||||
tlstate.loadDocument(mockDocument).deselectAll()
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
|
@ -167,7 +167,7 @@ describe('TLDrawState', () => {
|
|||
})
|
||||
|
||||
it('clears selection when clicking bounds', () => {
|
||||
tlstate.loadDocument(mockDocument).deselectAll()
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
tlstate.startSession(SessionType.Brush, [-10, -10])
|
||||
tlstate.updateSession([110, 110])
|
||||
tlstate.completeSession()
|
||||
|
@ -187,7 +187,7 @@ describe('TLDrawState', () => {
|
|||
// })
|
||||
|
||||
it('does not select on meta-click', () => {
|
||||
tlstate.loadDocument(mockDocument).deselectAll()
|
||||
tlstate.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1', { ctrlKey: true })
|
||||
expect(tlstate.selectedIds).toStrictEqual([])
|
||||
expect(tlstate.appState.status).toBe('idle')
|
||||
|
@ -214,7 +214,7 @@ describe('TLDrawState', () => {
|
|||
// Single click on a selected shape to select just that shape
|
||||
|
||||
it('single-selects shape in selection on click', () => {
|
||||
tlstate.deselectAll()
|
||||
tlstate.selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.clickShape('rect2')
|
||||
|
@ -223,7 +223,7 @@ describe('TLDrawState', () => {
|
|||
})
|
||||
|
||||
it('single-selects shape in selection on pointerup only', () => {
|
||||
tlstate.deselectAll()
|
||||
tlstate.selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.pointShape('rect2')
|
||||
|
@ -234,7 +234,7 @@ describe('TLDrawState', () => {
|
|||
})
|
||||
|
||||
// it('selects shapes if shift key is lifted before pointerup', () => {
|
||||
// tlstate.deselectAll()
|
||||
// tlstate.selectNone()
|
||||
// tlu.clickShape('rect1')
|
||||
// tlu.pointShape('rect2', { shiftKey: true })
|
||||
// expect(tlstate.appState.status).toBe('pointingBounds')
|
||||
|
@ -390,7 +390,7 @@ describe('TLDrawState', () => {
|
|||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
|
||||
const tlu = new TLDrawStateUtils(tlstate)
|
||||
|
||||
|
@ -454,7 +454,7 @@ describe('TLDrawState', () => {
|
|||
|
||||
expect(tlstate.getShape('groupA').childIndex).toBe(2)
|
||||
|
||||
tlstate.deselectAll()
|
||||
tlstate.selectNone()
|
||||
tlstate.selectTool(TLDrawShapeType.Rectangle)
|
||||
|
||||
const prevB = tlstate.shapes.map((shape) => shape.id)
|
||||
|
|
|
@ -685,7 +685,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
if (!document.pages[pageId]) {
|
||||
if (pageId === this.appState.currentPageId) {
|
||||
this.cancelSession()
|
||||
this.deselectAll()
|
||||
this.selectNone()
|
||||
}
|
||||
|
||||
currentPageStates[pageId] = undefined as unknown as TLPageState
|
||||
|
@ -812,7 +812,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
* @param document The document to load
|
||||
*/
|
||||
loadDocument = (document: TLDrawDocument): this => {
|
||||
this.deselectAll()
|
||||
this.selectNone()
|
||||
this.resetHistory()
|
||||
this.clearSelectHistory()
|
||||
this.session = undefined
|
||||
|
@ -1165,6 +1165,16 @@ export class TLDrawState extends StateManager<Data> {
|
|||
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.
|
||||
* @param point
|
||||
|
@ -1579,7 +1589,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
/**
|
||||
* Zoom the camera to 100%.
|
||||
*/
|
||||
zoomToActual = (): this => {
|
||||
resetZoom = (): this => {
|
||||
return this.zoomTo(1)
|
||||
}
|
||||
|
||||
|
@ -1714,7 +1724,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
/**
|
||||
* Deselect any selected shapes.
|
||||
*/
|
||||
deselectAll = (): this => {
|
||||
selectNone = (): this => {
|
||||
this.setSelectedIds([])
|
||||
this.addToSelectHistory(this.selectedIds)
|
||||
return this
|
||||
|
|
|
@ -73,7 +73,7 @@ describe('Delete command', () => {
|
|||
expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined)
|
||||
|
||||
tlstate
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.createShapes({
|
||||
id: 'arrow1',
|
||||
type: TLDrawShapeType.Arrow,
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('Group command', () => {
|
|||
|
||||
describe('when less than two shapes are selected', () => {
|
||||
it('does nothing', () => {
|
||||
tlstate.deselectAll()
|
||||
tlstate.selectNone()
|
||||
|
||||
// @ts-ignore
|
||||
const stackLength = tlstate.stack.length
|
||||
|
|
|
@ -47,12 +47,12 @@ describe('when running the command', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.rotate()
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
|
||||
tlstate.deselectAll().redo()
|
||||
tlstate.selectNone().redo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
})
|
||||
|
|
|
@ -72,12 +72,12 @@ describe('when running the command', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.stretch(StretchType.Horizontal)
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
|
||||
|
||||
tlstate.deselectAll().redo()
|
||||
tlstate.selectNone().redo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
|
||||
})
|
||||
|
|
|
@ -82,12 +82,12 @@ describe('when running the command', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.style({ size: SizeStyle.Small })
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
|
||||
tlstate.deselectAll().redo()
|
||||
tlstate.selectNone().redo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
})
|
||||
|
|
|
@ -67,12 +67,12 @@ describe('when running the command', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.toggleHidden()
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.undo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
|
||||
tlstate.deselectAll().redo()
|
||||
tlstate.selectNone().redo()
|
||||
|
||||
expect(tlstate.selectedIds).toEqual(['rect1'])
|
||||
})
|
||||
|
|
|
@ -6,7 +6,7 @@ describe('Brush session', () => {
|
|||
it('begins, updateSession', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.startSession(SessionType.Brush, [-10, -10])
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
@ -17,7 +17,7 @@ describe('Brush session', () => {
|
|||
it('selects multiple shapes', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.startSession(SessionType.Brush, [-10, -10])
|
||||
.updateSession([110, 110])
|
||||
.completeSession()
|
||||
|
@ -27,7 +27,7 @@ describe('Brush session', () => {
|
|||
it('does not de-select original shapes', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.select('rect1')
|
||||
.startSession(SessionType.Brush, [300, 300])
|
||||
.updateSession([301, 301])
|
||||
|
@ -38,9 +38,9 @@ describe('Brush session', () => {
|
|||
// it('does not select hidden shapes', () => {
|
||||
// const tlstate = new TLDrawState()
|
||||
// .loadDocument(mockDocument)
|
||||
// .deselectAll()
|
||||
// .selectNone()
|
||||
// .toggleHidden(['rect1'])
|
||||
// .deselectAll()
|
||||
// .selectNone()
|
||||
// .startSession(SessionType.Brush, [-10, -10])
|
||||
// .updateSession([10, 10])
|
||||
// .completeSession()
|
||||
|
@ -49,9 +49,9 @@ describe('Brush session', () => {
|
|||
it('when command is held, require the entire shape to be selected', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.selectNone()
|
||||
.startSession(SessionType.Brush, [-10, -10])
|
||||
.updateSession([10, 10], false, false, true)
|
||||
.completeSession()
|
||||
|
|
|
@ -50,7 +50,7 @@ export class DrawUtil extends TLDrawShapeUtil<T, E> {
|
|||
return style.dash === DashStyle.Draw
|
||||
? getDrawStrokePathData(shape)
|
||||
: getSolidStrokePathData(shape)
|
||||
}, [points, style.size, style.dash, isComplete, false])
|
||||
}, [points, style.size, style.dash, isComplete])
|
||||
|
||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ describe('When double clicking link controls', () => {
|
|||
.startSession(SessionType.Arrow, [200, 200], 'end')
|
||||
.updateSession([250, 50])
|
||||
.completeSession()
|
||||
.deselectAll().document
|
||||
.selectNone().document
|
||||
|
||||
it('moves all linked shapes when center is dragged', () => {
|
||||
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)
|
||||
}
|
||||
|
||||
private deselectAll() {
|
||||
this.state.deselectAll()
|
||||
private selectNone() {
|
||||
this.state.selectNone()
|
||||
}
|
||||
|
||||
onEnter = () => {
|
||||
|
@ -172,7 +172,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onCancel = () => {
|
||||
this.deselectAll()
|
||||
this.selectNone()
|
||||
this.state.cancelSession()
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
@ -379,7 +379,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
if (info.target === 'bounds') {
|
||||
// If we just clicked the selecting bounds's background,
|
||||
// clear the selection
|
||||
this.deselectAll()
|
||||
this.selectNone()
|
||||
} else if (this.state.isSelected(info.target)) {
|
||||
// If we're holding shift...
|
||||
if (info.shiftKey) {
|
||||
|
@ -438,7 +438,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
return
|
||||
}
|
||||
|
||||
this.deselectAll()
|
||||
this.selectNone()
|
||||
}
|
||||
|
||||
this.setStatus(Status.PointingCanvas)
|
||||
|
@ -494,7 +494,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
|
||||
if (info.metaKey) {
|
||||
if (!info.shiftKey) {
|
||||
this.deselectAll()
|
||||
this.selectNone()
|
||||
}
|
||||
|
||||
const point = this.state.getPagePoint(info.point)
|
||||
|
@ -595,7 +595,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
onPointBounds: TLBoundsEventHandler = (info) => {
|
||||
if (info.metaKey) {
|
||||
if (!info.shiftKey) {
|
||||
this.deselectAll()
|
||||
this.selectNone()
|
||||
}
|
||||
|
||||
const point = this.state.getPagePoint(info.point)
|
||||
|
|
Loading…
Reference in a new issue