[fix] text editing in vscode (#683)
* fix text editing events in vscode * fix outline in vscode
This commit is contained in:
parent
b8dfc9895a
commit
543757984b
5 changed files with 107 additions and 106 deletions
|
@ -1,9 +1,8 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
||||||
import { defaultTextStyle } from '../shared/shape-styles'
|
|
||||||
import { AlignStyle, StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
|
import { AlignStyle, StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
|
||||||
import { getBoundsRectangle, TextAreaUtils } from '../shared'
|
import { defaultTextStyle, getBoundsRectangle, TextAreaUtils } from '../shared'
|
||||||
import { TDShapeUtil } from '../TDShapeUtil'
|
import { TDShapeUtil } from '../TDShapeUtil'
|
||||||
import { getStickyFontStyle, getStickyShapeStyle } from '../shared/shape-styles'
|
import { getStickyFontStyle, getStickyShapeStyle } from '../shared/shape-styles'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
|
@ -59,24 +58,28 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
|
|
||||||
const rText = React.useRef<HTMLDivElement>(null)
|
const rText = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const rTextContent = React.useRef(shape.text)
|
|
||||||
|
|
||||||
const rIsMounted = React.useRef(false)
|
const rIsMounted = React.useRef(false)
|
||||||
|
|
||||||
const handlePointerDown = React.useCallback((e: React.PointerEvent) => {
|
const handlePointerDown = React.useCallback((e: React.PointerEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleTextChange = React.useCallback(
|
const onChange = React.useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(text: string) => {
|
||||||
rTextContent.current = TLDR.normalizeText(e.currentTarget.value)
|
|
||||||
onShapeChange?.({
|
onShapeChange?.({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: shape.type,
|
type: shape.type,
|
||||||
text: rTextContent.current,
|
text: TLDR.normalizeText(text),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[onShapeChange]
|
[shape.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTextChange = React.useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
onChange(e.currentTarget.value)
|
||||||
|
},
|
||||||
|
[onShapeChange, onChange]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
const handleKeyDown = React.useCallback(
|
||||||
|
@ -88,15 +91,6 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this keydown was just the meta key or a shortcut
|
|
||||||
// that includes holding the meta key like (Command+V)
|
|
||||||
// then leave the event untouched. We also have to explicitly
|
|
||||||
// Implement undo/redo for some reason in order to get this working
|
|
||||||
// in the vscode extension. Without the below code the following doesn't work
|
|
||||||
//
|
|
||||||
// - You can't cut/copy/paste when when text-editing/focused
|
|
||||||
// - You can't undo/redo when when text-editing/focused
|
|
||||||
// - You can't use Command+A to select all the text, when when text-editing/focused
|
|
||||||
if (!(e.key === 'Meta' || e.metaKey)) {
|
if (!(e.key === 'Meta' || e.metaKey)) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
} else if (e.key === 'z' && e.metaKey) {
|
} else if (e.key === 'z' && e.metaKey) {
|
||||||
|
@ -118,8 +112,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
TextAreaUtils.indent(e.currentTarget)
|
TextAreaUtils.indent(e.currentTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
rTextContent.current = TLDR.normalizeText(e.currentTarget.value)
|
onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) })
|
||||||
onShapeChange?.({ ...shape, text: rTextContent.current })
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[shape, onShapeChange]
|
[shape, onShapeChange]
|
||||||
|
@ -142,7 +135,6 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
// Focus when editing changes to true
|
// Focus when editing changes to true
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
rTextContent.current = shape.text
|
|
||||||
rIsMounted.current = true
|
rIsMounted.current = true
|
||||||
const elm = rTextArea.current!
|
const elm = rTextArea.current!
|
||||||
elm.focus()
|
elm.focus()
|
||||||
|
@ -210,13 +202,13 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
|
<StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
|
||||||
{isEditing ? rTextContent.current : shape.text}​
|
{shape.text}​
|
||||||
</StyledText>
|
</StyledText>
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<StyledTextArea
|
<StyledTextArea
|
||||||
ref={rTextArea}
|
ref={rTextArea}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
value={isEditing ? rTextContent.current : shape.text}
|
value={shape.text}
|
||||||
onChange={handleTextChange}
|
onChange={handleTextChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
|
@ -408,4 +400,8 @@ const StyledTextArea = styled('textarea', {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'&:focus': {
|
||||||
|
outline: 'none',
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
||||||
import { defaultTextStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
|
|
||||||
import { TextShape, TDMeta, TDShapeType, TransformInfo, AlignStyle } from '~types'
|
import { TextShape, TDMeta, TDShapeType, TransformInfo, AlignStyle } from '~types'
|
||||||
import { BINDING_DISTANCE, GHOSTED_OPACITY, LETTER_SPACING } from '~constants'
|
import { BINDING_DISTANCE, GHOSTED_OPACITY, LETTER_SPACING } from '~constants'
|
||||||
import { TDShapeUtil } from '../TDShapeUtil'
|
import { TDShapeUtil } from '../TDShapeUtil'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
import { getTextAlign } from '../shared/getTextAlign'
|
|
||||||
import { getTextSvgElement } from '../shared/getTextSvgElement'
|
|
||||||
import { stopPropagation } from '~components/stopPropagation'
|
import { stopPropagation } from '~components/stopPropagation'
|
||||||
import { useTextKeyboardEvents } from '../shared/useTextKeyboardEvents'
|
import {
|
||||||
import { preventEvent } from '~components/preventEvent'
|
getTextSvgElement,
|
||||||
|
TextAreaUtils,
|
||||||
|
defaultTextStyle,
|
||||||
|
getShapeStyle,
|
||||||
|
getFontStyle,
|
||||||
|
getTextAlign,
|
||||||
|
} from '../shared'
|
||||||
|
|
||||||
type T = TextShape
|
type T = TextShape
|
||||||
type E = HTMLDivElement
|
type E = HTMLDivElement
|
||||||
|
@ -90,15 +93,41 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
[shape.id, shape.point]
|
[shape.id, shape.point]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onChange = React.useCallback(
|
const handleKeyDown = React.useCallback(
|
||||||
(text: string) => {
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
this.texts.set(shape.id, TLDR.normalizeText(text))
|
if (e.key === 'Escape') return
|
||||||
onShapeChange?.({ id: shape.id, text: this.texts.get(shape.id)! })
|
|
||||||
},
|
|
||||||
[shape.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleKeyDown = useTextKeyboardEvents(onChange)
|
if (e.key === 'Tab' && shape.text.length === 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(e.key === 'Meta' || e.metaKey)) {
|
||||||
|
e.stopPropagation()
|
||||||
|
} else if (e.key === 'z' && e.metaKey) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
document.execCommand('redo', false)
|
||||||
|
} else {
|
||||||
|
document.execCommand('undo', false)
|
||||||
|
}
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.shiftKey) {
|
||||||
|
TextAreaUtils.unindent(e.currentTarget)
|
||||||
|
} else {
|
||||||
|
TextAreaUtils.indent(e.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[shape, onShapeChange]
|
||||||
|
)
|
||||||
|
|
||||||
const handleBlur = React.useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
|
const handleBlur = React.useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
e.currentTarget.setSelectionRange(0, 0)
|
e.currentTarget.setSelectionRange(0, 0)
|
||||||
|
@ -434,4 +463,8 @@ const TextArea = styled('textarea', {
|
||||||
userSelect: 'text',
|
userSelect: 'text',
|
||||||
WebkitUserSelect: 'text',
|
WebkitUserSelect: 'text',
|
||||||
...commonTextWrapping,
|
...commonTextWrapping,
|
||||||
|
'&:focus': {
|
||||||
|
outline: 'none',
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { GHOSTED_OPACITY, LETTER_SPACING } from '~constants'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import { getTextLabelSize } from './getTextSize'
|
import { getTextLabelSize } from './getTextSize'
|
||||||
import { useTextKeyboardEvents } from './useTextKeyboardEvents'
|
import { TextAreaUtils } from './TextAreaUtils'
|
||||||
|
|
||||||
export interface TextLabelProps {
|
export interface TextLabelProps {
|
||||||
font: string
|
font: string
|
||||||
|
@ -32,17 +32,47 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
const rInput = React.useRef<HTMLTextAreaElement>(null)
|
const rInput = React.useRef<HTMLTextAreaElement>(null)
|
||||||
const rIsMounted = React.useRef(false)
|
const rIsMounted = React.useRef(false)
|
||||||
|
|
||||||
const rTextContent = React.useRef(text)
|
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
rTextContent.current = TLDR.normalizeText(e.currentTarget.value)
|
onChange(TLDR.normalizeText(e.currentTarget.value))
|
||||||
onChange(rTextContent.current)
|
|
||||||
},
|
},
|
||||||
[onChange]
|
[onChange]
|
||||||
)
|
)
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Escape') return
|
||||||
|
|
||||||
const handleKeyDown = useTextKeyboardEvents(onChange)
|
if (e.key === 'Tab' && text.length === 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(e.key === 'Meta' || e.metaKey)) {
|
||||||
|
e.stopPropagation()
|
||||||
|
} else if (e.key === 'z' && e.metaKey) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
document.execCommand('redo', false)
|
||||||
|
} else {
|
||||||
|
document.execCommand('undo', false)
|
||||||
|
}
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.shiftKey) {
|
||||||
|
TextAreaUtils.unindent(e.currentTarget)
|
||||||
|
} else {
|
||||||
|
TextAreaUtils.indent(e.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange?.(TLDR.normalizeText(e.currentTarget.value))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
)
|
||||||
|
|
||||||
const handleBlur = React.useCallback(
|
const handleBlur = React.useCallback(
|
||||||
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
|
@ -75,7 +105,6 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
rTextContent.current = text
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
rIsMounted.current = true
|
rIsMounted.current = true
|
||||||
const elm = rInput.current
|
const elm = rInput.current
|
||||||
|
@ -130,7 +159,7 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
wrap="off"
|
wrap="off"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
datatype="wysiwyg"
|
datatype="wysiwyg"
|
||||||
defaultValue={rTextContent.current}
|
defaultValue={text}
|
||||||
color={color}
|
color={color}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
@ -238,4 +267,8 @@ const TextArea = styled('textarea', {
|
||||||
WebkitFontSmoothing: 'subpixel-antialiased',
|
WebkitFontSmoothing: 'subpixel-antialiased',
|
||||||
MozOsxFontSmoothing: 'auto',
|
MozOsxFontSmoothing: 'auto',
|
||||||
...commonTextWrapping,
|
...commonTextWrapping,
|
||||||
|
'&:focus': {
|
||||||
|
outline: 'none',
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,3 +5,4 @@ export * from './TextAreaUtils'
|
||||||
export * from './shape-styles'
|
export * from './shape-styles'
|
||||||
export * from './getTextAlign'
|
export * from './getTextAlign'
|
||||||
export * from './TextLabel'
|
export * from './TextLabel'
|
||||||
|
export * from './getTextSvgElement'
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import * as React from 'react'
|
|
||||||
import { TLDR } from '~state/TLDR'
|
|
||||||
import { TextAreaUtils } from '.'
|
|
||||||
|
|
||||||
export function useTextKeyboardEvents(onChange: (text: string) => void) {
|
|
||||||
const handleKeyDown = React.useCallback(
|
|
||||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
// If this keydown was just the meta key or a shortcut
|
|
||||||
// that includes holding the meta key like (Command+V)
|
|
||||||
// then leave the event untouched. We also have to explicitly
|
|
||||||
// Implement undo/redo for some reason in order to get this working
|
|
||||||
// in the vscode extension. Without the below code the following doesn't work
|
|
||||||
//
|
|
||||||
// - You can't cut/copy/paste when when text-editing/focused
|
|
||||||
// - You can't undo/redo when when text-editing/focused
|
|
||||||
// - You can't use Command+A to select all the text, when when text-editing/focused
|
|
||||||
if (e.metaKey) e.stopPropagation()
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Meta': {
|
|
||||||
e.stopPropagation()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'z': {
|
|
||||||
if (e.metaKey) {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
document.execCommand('redo', false)
|
|
||||||
} else {
|
|
||||||
document.execCommand('undo', false)
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Escape': {
|
|
||||||
e.currentTarget.blur()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Enter': {
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
e.currentTarget.blur()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Tab': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (e.shiftKey) {
|
|
||||||
TextAreaUtils.unindent(e.currentTarget)
|
|
||||||
} else {
|
|
||||||
TextAreaUtils.indent(e.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(TLDR.normalizeText(e.currentTarget.value))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
)
|
|
||||||
|
|
||||||
return handleKeyDown
|
|
||||||
}
|
|
Loading…
Reference in a new issue