Add "paste at cursor" option, which toggles how cmd + v
and cmd + shift + v
work (#4088)
Add an option to make paste at cursor the default. Not sure if we also want to expose this on tldraw.com? For now I did, but happy to remove if we'd want to keep the preferences simple. We could also add this to the `TldrawOptions`, but it felt like some apps might actually allow this customization on a per user level. Solves https://github.com/tldraw/tldraw/issues/4066 ### Change type - [ ] `bugfix` - [ ] `improvement` - [x] `feature` - [ ] `api` - [ ] `other` ### Test plan 1. Copy / pasting should still work as it works now: `⌘ + v` pastes on top of the shape, `⌘ + ⇧ + v` pastes at cursor. 2. There's now a new option under Preferences to paste at cursor. This just swaps the logic between the two shortcuts: `⌘ + v` then pastes at cursor and `⌘ + ⇧ + v` pastes on top of the shape. ### Release notes - Allow users and sdk users to make pasting at the cursor a default instead of only being available with `⌘ + ⇧ + v`.
This commit is contained in:
parent
7d5a6cbe3b
commit
a85c215ffc
12 changed files with 61 additions and 3 deletions
|
@ -83,6 +83,8 @@
|
||||||
"action.toggle-auto-size": "Toggle auto size",
|
"action.toggle-auto-size": "Toggle auto size",
|
||||||
"action.toggle-dark-mode.menu": "Dark mode",
|
"action.toggle-dark-mode.menu": "Dark mode",
|
||||||
"action.toggle-dark-mode": "Toggle dark mode",
|
"action.toggle-dark-mode": "Toggle dark mode",
|
||||||
|
"action.toggle-paste-at-cursor.menu": "Paste at cursor",
|
||||||
|
"action.toggle-paste-at-cursor": "Toggle paste at cursor",
|
||||||
"action.toggle-wrap-mode.menu": "Select on wrap",
|
"action.toggle-wrap-mode.menu": "Select on wrap",
|
||||||
"action.toggle-wrap-mode": "Toggle Select on wrap",
|
"action.toggle-wrap-mode": "Toggle Select on wrap",
|
||||||
"action.toggle-reduce-motion.menu": "Reduce motion",
|
"action.toggle-reduce-motion.menu": "Reduce motion",
|
||||||
|
|
|
@ -724,6 +724,7 @@ export const defaultUserPreferences: Readonly<{
|
||||||
isWrapMode: false;
|
isWrapMode: false;
|
||||||
locale: "ar" | "ca" | "cs" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw";
|
locale: "ar" | "ca" | "cs" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw";
|
||||||
name: "New User";
|
name: "New User";
|
||||||
|
pasteAtCursor: false;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
|
@ -3371,6 +3372,8 @@ export interface TLUserPreferences {
|
||||||
locale?: null | string;
|
locale?: null | string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
name?: null | string;
|
name?: null | string;
|
||||||
|
// (undocumented)
|
||||||
|
pasteAtCursor?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -3480,6 +3483,8 @@ export class UserPreferencesManager {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getName(): string;
|
getName(): string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getPasteAtCursor(): boolean;
|
||||||
|
// (undocumented)
|
||||||
getUserPreferences(): {
|
getUserPreferences(): {
|
||||||
animationSpeed: number;
|
animationSpeed: number;
|
||||||
color: string;
|
color: string;
|
||||||
|
|
|
@ -22,6 +22,7 @@ export interface TLUserPreferences {
|
||||||
isSnapMode?: boolean | null
|
isSnapMode?: boolean | null
|
||||||
isWrapMode?: boolean | null
|
isWrapMode?: boolean | null
|
||||||
isDynamicSizeMode?: boolean | null
|
isDynamicSizeMode?: boolean | null
|
||||||
|
pasteAtCursor?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDataSnapshot {
|
interface UserDataSnapshot {
|
||||||
|
@ -46,6 +47,7 @@ const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUserPrefere
|
||||||
isSnapMode: T.boolean.nullable().optional(),
|
isSnapMode: T.boolean.nullable().optional(),
|
||||||
isWrapMode: T.boolean.nullable().optional(),
|
isWrapMode: T.boolean.nullable().optional(),
|
||||||
isDynamicSizeMode: T.boolean.nullable().optional(),
|
isDynamicSizeMode: T.boolean.nullable().optional(),
|
||||||
|
pasteAtCursor: T.boolean.nullable().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const Versions = {
|
const Versions = {
|
||||||
|
@ -56,6 +58,7 @@ const Versions = {
|
||||||
AddExcalidrawSelectMode: 5,
|
AddExcalidrawSelectMode: 5,
|
||||||
AddDynamicSizeMode: 6,
|
AddDynamicSizeMode: 6,
|
||||||
AllowSystemColorScheme: 7,
|
AllowSystemColorScheme: 7,
|
||||||
|
AddPasteAtCursor: 8,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const CURRENT_VERSION = Math.max(...Object.values(Versions))
|
const CURRENT_VERSION = Math.max(...Object.values(Versions))
|
||||||
|
@ -88,6 +91,9 @@ function migrateSnapshot(data: { version: number; user: any }) {
|
||||||
if (data.version < Versions.AddDynamicSizeMode) {
|
if (data.version < Versions.AddDynamicSizeMode) {
|
||||||
data.user.isDynamicSizeMode = false
|
data.user.isDynamicSizeMode = false
|
||||||
}
|
}
|
||||||
|
if (data.version < Versions.AddPasteAtCursor) {
|
||||||
|
data.user.pasteAtCursor = false
|
||||||
|
}
|
||||||
|
|
||||||
// finally
|
// finally
|
||||||
data.version = CURRENT_VERSION
|
data.version = CURRENT_VERSION
|
||||||
|
@ -131,6 +137,7 @@ export const defaultUserPreferences = Object.freeze({
|
||||||
isSnapMode: false,
|
isSnapMode: false,
|
||||||
isWrapMode: false,
|
isWrapMode: false,
|
||||||
isDynamicSizeMode: false,
|
isDynamicSizeMode: false,
|
||||||
|
pasteAtCursor: false,
|
||||||
}) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
|
}) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
|
@ -97,4 +97,8 @@ export class UserPreferencesManager {
|
||||||
this.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode
|
this.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed getPasteAtCursor() {
|
||||||
|
return this.user.userPreferences.get().pasteAtCursor ?? defaultUserPreferences.pasteAtCursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -21,6 +21,7 @@ import {
|
||||||
ToggleFocusModeItem,
|
ToggleFocusModeItem,
|
||||||
ToggleGridItem,
|
ToggleGridItem,
|
||||||
ToggleLockMenuItem,
|
ToggleLockMenuItem,
|
||||||
|
TogglePasteAtCursorItem,
|
||||||
ToggleReduceMotionItem,
|
ToggleReduceMotionItem,
|
||||||
ToggleSnapModeItem,
|
ToggleSnapModeItem,
|
||||||
ToggleToolLockItem,
|
ToggleToolLockItem,
|
||||||
|
@ -174,6 +175,7 @@ export function PreferencesGroup() {
|
||||||
<ToggleEdgeScrollingItem />
|
<ToggleEdgeScrollingItem />
|
||||||
<ToggleReduceMotionItem />
|
<ToggleReduceMotionItem />
|
||||||
<ToggleDynamicSizeModeItem />
|
<ToggleDynamicSizeModeItem />
|
||||||
|
<TogglePasteAtCursorItem />
|
||||||
<ToggleDebugModeItem />
|
<ToggleDebugModeItem />
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
<TldrawUiMenuGroup id="color-scheme">
|
<TldrawUiMenuGroup id="color-scheme">
|
||||||
|
|
|
@ -626,6 +626,14 @@ export function ToggleDynamicSizeModeItem() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public @react */
|
||||||
|
export function TogglePasteAtCursorItem() {
|
||||||
|
const actions = useActions()
|
||||||
|
const editor = useEditor()
|
||||||
|
const pasteAtCursor = useValue('paste at cursor', () => editor.user.getPasteAtCursor(), [editor])
|
||||||
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-paste-at-cursor']} checked={pasteAtCursor} />
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------------- Print --------------------- */
|
/* ---------------------- Print --------------------- */
|
||||||
/** @public @react */
|
/** @public @react */
|
||||||
export function PrintItem() {
|
export function PrintItem() {
|
||||||
|
|
|
@ -1149,6 +1149,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
},
|
},
|
||||||
checkbox: true,
|
checkbox: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'toggle-paste-at-cursor',
|
||||||
|
label: {
|
||||||
|
default: 'action.toggle-paste-at-cursor',
|
||||||
|
menu: 'action.toggle-paste-at-cursor.menu',
|
||||||
|
},
|
||||||
|
readonlyOk: false,
|
||||||
|
onSelect(source) {
|
||||||
|
trackEvent('toggle-paste-at-cursor', { source })
|
||||||
|
editor.user.updateUserPreferences({
|
||||||
|
pasteAtCursor: !editor.user.getPasteAtCursor(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
checkbox: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'toggle-reduce-motion',
|
id: 'toggle-reduce-motion',
|
||||||
label: {
|
label: {
|
||||||
|
|
|
@ -88,6 +88,7 @@ export interface TLUiEventMap {
|
||||||
'toggle-focus-mode': null
|
'toggle-focus-mode': null
|
||||||
'toggle-debug-mode': null
|
'toggle-debug-mode': null
|
||||||
'toggle-dynamic-size-mode': null
|
'toggle-dynamic-size-mode': null
|
||||||
|
'toggle-paste-at-cursor': null
|
||||||
'toggle-lock': null
|
'toggle-lock': null
|
||||||
'toggle-reduce-motion': null
|
'toggle-reduce-motion': null
|
||||||
'toggle-edge-scrolling': null
|
'toggle-edge-scrolling': null
|
||||||
|
|
|
@ -671,12 +671,20 @@ export function useNativeClipboardEvents() {
|
||||||
|
|
||||||
// First try to use the clipboard data on the event
|
// First try to use the clipboard data on the event
|
||||||
if (e.clipboardData && !editor.inputs.shiftKey) {
|
if (e.clipboardData && !editor.inputs.shiftKey) {
|
||||||
handlePasteFromEventClipboardData(editor, e.clipboardData)
|
if (editor.user.getPasteAtCursor()) {
|
||||||
|
handlePasteFromEventClipboardData(editor, e.clipboardData, editor.inputs.currentPagePoint)
|
||||||
|
} else {
|
||||||
|
handlePasteFromEventClipboardData(editor, e.clipboardData)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Or else use the clipboard API
|
// Or else use the clipboard API
|
||||||
navigator.clipboard.read().then((clipboardItems) => {
|
navigator.clipboard.read().then((clipboardItems) => {
|
||||||
if (Array.isArray(clipboardItems) && clipboardItems[0] instanceof ClipboardItem) {
|
if (Array.isArray(clipboardItems) && clipboardItems[0] instanceof ClipboardItem) {
|
||||||
handlePasteFromClipboardApi(editor, clipboardItems, editor.inputs.currentPagePoint)
|
if (e.clipboardData && editor.user.getPasteAtCursor()) {
|
||||||
|
handlePasteFromClipboardApi(editor, clipboardItems)
|
||||||
|
} else {
|
||||||
|
handlePasteFromClipboardApi(editor, clipboardItems, editor.inputs.currentPagePoint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,8 @@ export type TLUiTranslationKey =
|
||||||
| 'action.toggle-auto-size'
|
| 'action.toggle-auto-size'
|
||||||
| 'action.toggle-dark-mode.menu'
|
| 'action.toggle-dark-mode.menu'
|
||||||
| 'action.toggle-dark-mode'
|
| 'action.toggle-dark-mode'
|
||||||
|
| 'action.toggle-paste-at-cursor.menu'
|
||||||
|
| 'action.toggle-paste-at-cursor'
|
||||||
| 'action.toggle-wrap-mode.menu'
|
| 'action.toggle-wrap-mode.menu'
|
||||||
| 'action.toggle-wrap-mode'
|
| 'action.toggle-wrap-mode'
|
||||||
| 'action.toggle-reduce-motion.menu'
|
| 'action.toggle-reduce-motion.menu'
|
||||||
|
|
|
@ -87,6 +87,8 @@ export const DEFAULT_TRANSLATION = {
|
||||||
'action.toggle-auto-size': 'Toggle auto size',
|
'action.toggle-auto-size': 'Toggle auto size',
|
||||||
'action.toggle-dark-mode.menu': 'Dark mode',
|
'action.toggle-dark-mode.menu': 'Dark mode',
|
||||||
'action.toggle-dark-mode': 'Toggle dark mode',
|
'action.toggle-dark-mode': 'Toggle dark mode',
|
||||||
|
'action.toggle-paste-at-cursor.menu': 'Paste at cursor',
|
||||||
|
'action.toggle-paste-at-cursor': 'Toggle paste at cursor',
|
||||||
'action.toggle-wrap-mode.menu': 'Select on wrap',
|
'action.toggle-wrap-mode.menu': 'Select on wrap',
|
||||||
'action.toggle-wrap-mode': 'Toggle Select on wrap',
|
'action.toggle-wrap-mode': 'Toggle Select on wrap',
|
||||||
'action.toggle-reduce-motion.menu': 'Reduce motion',
|
'action.toggle-reduce-motion.menu': 'Reduce motion',
|
||||||
|
|
Loading…
Reference in a new issue