[fix] text editing in vscode (#683)

* fix text editing events in vscode

* fix outline in vscode
This commit is contained in:
Steve Ruiz 2022-05-14 11:43:56 +01:00 committed by GitHub
parent b8dfc9895a
commit 543757984b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 106 deletions

View file

@ -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}&#8203; {shape.text}&#8203;
</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',
},
}) })

View file

@ -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',
},
}) })

View file

@ -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',
},
}) })

View file

@ -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'

View file

@ -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
}