emojis! 🧑🎨 🎨 ✏️ (#2814)
everyone ❤️'s emojis: https://dropbox.tech/application/dropbox-paper-emojis-and-exformation https://github.com/tldraw/tldraw/assets/469604/8f99f485-de98-44d1-93cb-6eb9c2d87d99 ### Change Type - [x] `minor` — New feature ### Test Plan 1. Test adding lots of emojis! ### Release Notes - Adds emoji picker to text fields.
This commit is contained in:
parent
c4ffa05b12
commit
32f641c1d7
17 changed files with 426 additions and 15 deletions
BIN
.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch
Normal file
BIN
.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch
Normal file
Binary file not shown.
|
@ -1140,6 +1140,8 @@ export class Group2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getArea(): number;
|
getArea(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getLabel(): Geometry2d;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
||||||
|
|
|
@ -22688,6 +22688,38 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getArea"
|
"name": "getArea"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Method",
|
||||||
|
"canonicalReference": "@tldraw/editor!Group2d#getLabel:member(1)",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "getLabel(): "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "Geometry2d",
|
||||||
|
"canonicalReference": "@tldraw/editor!Geometry2d:class"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isStatic": false,
|
||||||
|
"returnTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2
|
||||||
|
},
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"isProtected": false,
|
||||||
|
"overloadIndex": 1,
|
||||||
|
"parameters": [],
|
||||||
|
"isOptional": false,
|
||||||
|
"isAbstract": false,
|
||||||
|
"name": "getLabel"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Method",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Group2d#getVertices:member(1)",
|
"canonicalReference": "@tldraw/editor!Group2d#getVertices:member(1)",
|
||||||
|
|
|
@ -70,6 +70,10 @@ export class Group2d extends Geometry2d {
|
||||||
return this.children[0].area
|
return this.children[0].area
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLabel() {
|
||||||
|
return this.children.filter((c) => c.isLabel)[0]
|
||||||
|
}
|
||||||
|
|
||||||
toSimpleSvgPath() {
|
toSimpleSvgPath() {
|
||||||
let path = ''
|
let path = ''
|
||||||
for (const child of this.children) {
|
for (const child of this.children) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { Atom, atom, react } from '@tldraw/state'
|
||||||
// `true` by default in development and staging, and `false` in production.
|
// `true` by default in development and staging, and `false` in production.
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const featureFlags: Record<string, DebugFlag<boolean>> = {
|
export const featureFlags: Record<string, DebugFlag<boolean>> = {
|
||||||
// canMoveArrowLabel: createFeatureFlag('canMoveArrowLabel'),
|
emojiMenu: createFeatureFlag('emojiMenu'),
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -111,16 +111,16 @@ function createDebugValue<T>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// function createFeatureFlag(
|
function createFeatureFlag(
|
||||||
// name: string,
|
name: string,
|
||||||
// defaults: Defaults<boolean> = { all: true, production: false }
|
defaults: Defaults<boolean> = { all: true, production: false }
|
||||||
// ) {
|
) {
|
||||||
// return createDebugValueBase({
|
return createDebugValueBase({
|
||||||
// name,
|
name,
|
||||||
// defaults,
|
defaults,
|
||||||
// shouldStoreForSession: true,
|
shouldStoreForSession: true,
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
|
|
||||||
function createDebugValueBase<T>(def: DebugFlagDef<T>): DebugFlag<T> {
|
function createDebugValueBase<T>(def: DebugFlagDef<T>): DebugFlag<T> {
|
||||||
const defaultValue = getDefaultValue(def)
|
const defaultValue = getDefaultValue(def)
|
||||||
|
|
|
@ -1420,8 +1420,12 @@ export interface TLUiDialog {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component: (props: TLUiDialogProps) => any;
|
component: (props: TLUiDialogProps) => any;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
dialogProps?: any;
|
||||||
|
// (undocumented)
|
||||||
id: string;
|
id: string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
isCustomDialog?: boolean;
|
||||||
|
// (undocumented)
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16213,6 +16213,33 @@
|
||||||
"endIndex": 4
|
"endIndex": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "PropertySignature",
|
||||||
|
"canonicalReference": "@tldraw/tldraw!TLUiDialog#dialogProps:member",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "dialogProps?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isReadonly": false,
|
||||||
|
"isOptional": true,
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "dialogProps",
|
||||||
|
"propertyTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "PropertySignature",
|
"kind": "PropertySignature",
|
||||||
"canonicalReference": "@tldraw/tldraw!TLUiDialog#id:member",
|
"canonicalReference": "@tldraw/tldraw!TLUiDialog#id:member",
|
||||||
|
@ -16240,6 +16267,33 @@
|
||||||
"endIndex": 2
|
"endIndex": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "PropertySignature",
|
||||||
|
"canonicalReference": "@tldraw/tldraw!TLUiDialog#isCustomDialog:member",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "isCustomDialog?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isReadonly": false,
|
||||||
|
"isOptional": true,
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "isCustomDialog",
|
||||||
|
"propertyTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "PropertySignature",
|
"kind": "PropertySignature",
|
||||||
"canonicalReference": "@tldraw/tldraw!TLUiDialog#onClose:member",
|
"canonicalReference": "@tldraw/tldraw!TLUiDialog#onClose:member",
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"tldraw.css"
|
"tldraw.css"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-context-menu": "^2.1.5",
|
"@radix-ui/react-context-menu": "^2.1.5",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
@ -56,6 +57,7 @@
|
||||||
"@tldraw/editor": "workspace:*",
|
"@tldraw/editor": "workspace:*",
|
||||||
"canvas-size": "^1.2.6",
|
"canvas-size": "^1.2.6",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
|
"emoji-mart": "patch:emoji-mart@npm%3A5.5.2#~/.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch",
|
||||||
"hotkeys-js": "^3.11.2",
|
"hotkeys-js": "^3.11.2",
|
||||||
"lz-string": "^1.4.4"
|
"lz-string": "^1.4.4"
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { TextLabel } from '../shared/TextLabel'
|
||||||
import {
|
import {
|
||||||
FONT_FAMILIES,
|
FONT_FAMILIES,
|
||||||
LABEL_FONT_SIZES,
|
LABEL_FONT_SIZES,
|
||||||
|
LABEL_PADDING,
|
||||||
STROKE_SIZES,
|
STROKE_SIZES,
|
||||||
TEXT_PROPS,
|
TEXT_PROPS,
|
||||||
} from '../shared/default-shape-constants'
|
} from '../shared/default-shape-constants'
|
||||||
|
@ -59,7 +60,6 @@ import {
|
||||||
} from './components/SolidStyleOval'
|
} from './components/SolidStyleOval'
|
||||||
import { SolidStylePolygon, SolidStylePolygonSvg } from './components/SolidStylePolygon'
|
import { SolidStylePolygon, SolidStylePolygonSvg } from './components/SolidStylePolygon'
|
||||||
|
|
||||||
const LABEL_PADDING = 16
|
|
||||||
const MIN_SIZE_WITH_LABEL = 17 * 3
|
const MIN_SIZE_WITH_LABEL = 17 * 3
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
|
@ -58,4 +58,6 @@ export const LABEL_TO_ARROW_PADDING = 20
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const ARROW_LABEL_PADDING = 4.25
|
export const ARROW_LABEL_PADDING = 4.25
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
export const LABEL_PADDING = 16
|
||||||
|
/** @internal */
|
||||||
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10
|
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10
|
||||||
|
|
68
packages/tldraw/src/lib/shapes/shared/emojis/EmojiDialog.tsx
Normal file
68
packages/tldraw/src/lib/shapes/shared/emojis/EmojiDialog.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { TLEventInfo, track, useEditor } from '@tldraw/editor'
|
||||||
|
import { Picker } from 'emoji-mart'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { useDefaultColorTheme } from '../ShapeFill'
|
||||||
|
|
||||||
|
export type EmojiDialogProps = {
|
||||||
|
onClose: () => void
|
||||||
|
text: string
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
onEmojiSelect: (emoji: any) => void
|
||||||
|
onClickOutside: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default track(function EmojiDialog({
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
onEmojiSelect,
|
||||||
|
onClickOutside,
|
||||||
|
}: EmojiDialogProps) {
|
||||||
|
const editor = useEditor()
|
||||||
|
const theme = useDefaultColorTheme()
|
||||||
|
const ref = useRef(null)
|
||||||
|
const instance = useRef<any>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const eventListener = (event: TLEventInfo) => {
|
||||||
|
if (event.name === 'pointer_down') {
|
||||||
|
onClickOutside()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.on('event', eventListener)
|
||||||
|
|
||||||
|
instance.current = new Picker({
|
||||||
|
maxFrequentRows: 0,
|
||||||
|
onEmojiSelect,
|
||||||
|
onClickOutside,
|
||||||
|
theme: theme.id,
|
||||||
|
searchPosition: 'static',
|
||||||
|
previewPosition: 'none',
|
||||||
|
ref,
|
||||||
|
})
|
||||||
|
EmojiDialogSingleton = instance.current
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
instance.current = null
|
||||||
|
EmojiDialogSingleton = null
|
||||||
|
editor.off('event', eventListener)
|
||||||
|
}
|
||||||
|
}, [editor, theme.id, onEmojiSelect, onClickOutside])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
zIndex: 400,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
transformOrigin: '0 0',
|
||||||
|
transform: 'scale(0.85)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export let EmojiDialogSingleton: { component: any } | null = null
|
11
packages/tldraw/src/lib/shapes/shared/emojis/index.tsx
Normal file
11
packages/tldraw/src/lib/shapes/shared/emojis/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Suspense, lazy } from 'react'
|
||||||
|
|
||||||
|
const EmojiDialog = lazy(() => import('./EmojiDialog'))
|
||||||
|
|
||||||
|
export default function EmojiDialogLazy(props: any) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div />}>
|
||||||
|
<EmojiDialog {...props} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,8 +1,15 @@
|
||||||
/* eslint-disable no-inner-declarations */
|
/* eslint-disable no-inner-declarations */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Editor,
|
||||||
|
Group2d,
|
||||||
|
Rectangle2d,
|
||||||
|
TLArrowShape,
|
||||||
|
TLGeoShape,
|
||||||
TLShape,
|
TLShape,
|
||||||
|
TLTextShape,
|
||||||
TLUnknownShape,
|
TLUnknownShape,
|
||||||
|
featureFlags,
|
||||||
getPointerInfo,
|
getPointerInfo,
|
||||||
preventDefault,
|
preventDefault,
|
||||||
stopEventPropagation,
|
stopEventPropagation,
|
||||||
|
@ -11,6 +18,16 @@ import {
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import React, { useCallback, useEffect, useRef } from 'react'
|
import React, { useCallback, useEffect, useRef } from 'react'
|
||||||
import { INDENT, TextHelpers } from './TextHelpers'
|
import { INDENT, TextHelpers } from './TextHelpers'
|
||||||
|
import {
|
||||||
|
ARROW_LABEL_FONT_SIZES,
|
||||||
|
ARROW_LABEL_PADDING,
|
||||||
|
FONT_FAMILIES,
|
||||||
|
FONT_SIZES,
|
||||||
|
LABEL_FONT_SIZES,
|
||||||
|
LABEL_PADDING,
|
||||||
|
TEXT_PROPS,
|
||||||
|
} from './default-shape-constants'
|
||||||
|
import { useEmojis } from './useEmojis'
|
||||||
|
|
||||||
export function useEditableText<T extends Extract<TLShape, { props: { text: string } }>>(
|
export function useEditableText<T extends Extract<TLShape, { props: { text: string } }>>(
|
||||||
id: T['id'],
|
id: T['id'],
|
||||||
|
@ -18,8 +35,12 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
||||||
text: string
|
text: string
|
||||||
) {
|
) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const rInput = useRef<HTMLTextAreaElement>(null)
|
const rInput = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const { onKeyDown: onEmojiKeyDown } = useEmojis(rInput.current, (text: string) => {
|
||||||
|
editor.updateShapes<TLUnknownShape & { props: { text: string } }>([
|
||||||
|
{ id, type, props: { text } },
|
||||||
|
])
|
||||||
|
})
|
||||||
const rSkipSelectOnFocus = useRef(false)
|
const rSkipSelectOnFocus = useRef(false)
|
||||||
const rSelectionRanges = useRef<Range[] | null>()
|
const rSelectionRanges = useRef<Range[] | null>()
|
||||||
|
|
||||||
|
@ -101,6 +122,14 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
||||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (!isEditing) return
|
if (!isEditing) return
|
||||||
|
|
||||||
|
if (featureFlags.emojiMenu.get()) {
|
||||||
|
const coords = getCaretPosition(editor, e.target as HTMLTextAreaElement)
|
||||||
|
const isHandledByEmoji = onEmojiKeyDown(e, coords)
|
||||||
|
if (isHandledByEmoji) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
@ -119,7 +148,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[editor, isEditing]
|
[editor, isEditing, onEmojiKeyDown]
|
||||||
)
|
)
|
||||||
|
|
||||||
// When the text changes, update the text value.
|
// When the text changes, update the text value.
|
||||||
|
@ -207,3 +236,46 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
||||||
isEmpty,
|
isEmpty,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCaretPosition(editor: Editor, inputEl: HTMLTextAreaElement | null) {
|
||||||
|
const selectedShape = editor.getOnlySelectedShape() as
|
||||||
|
| TLTextShape
|
||||||
|
| TLArrowShape
|
||||||
|
| TLGeoShape
|
||||||
|
| undefined
|
||||||
|
if (!selectedShape) return null
|
||||||
|
|
||||||
|
let labelX, labelY
|
||||||
|
if (selectedShape.type === 'text') {
|
||||||
|
labelX = selectedShape.x
|
||||||
|
labelY = selectedShape.y
|
||||||
|
} else {
|
||||||
|
const geometry = editor.getShapeGeometry(selectedShape)
|
||||||
|
if (!(geometry instanceof Group2d)) return null
|
||||||
|
const labelGeometry = geometry.getLabel() as Rectangle2d
|
||||||
|
const padding = selectedShape.type === 'arrow' ? ARROW_LABEL_PADDING : LABEL_PADDING
|
||||||
|
labelX = selectedShape.x + labelGeometry.x + padding
|
||||||
|
labelY = selectedShape.y + labelGeometry.y + padding
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeSet =
|
||||||
|
selectedShape.type === 'arrow'
|
||||||
|
? ARROW_LABEL_FONT_SIZES
|
||||||
|
: selectedShape.type === 'text'
|
||||||
|
? FONT_SIZES
|
||||||
|
: LABEL_FONT_SIZES
|
||||||
|
const substring = !inputEl ? '' : inputEl.value.substring(0, inputEl.selectionStart)
|
||||||
|
const { w, h } = editor.textMeasure.measureText(substring, {
|
||||||
|
...TEXT_PROPS,
|
||||||
|
fontFamily: FONT_FAMILIES[selectedShape.props.font],
|
||||||
|
fontSize: sizeSet[selectedShape.props.size],
|
||||||
|
maxWidth: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { x, y } = editor.pageToScreen({ x: labelX, y: labelY })
|
||||||
|
const zoomLevel = editor.getZoomLevel()
|
||||||
|
const top = y + h * zoomLevel
|
||||||
|
const left = x + w * zoomLevel
|
||||||
|
|
||||||
|
return { top, left }
|
||||||
|
}
|
||||||
|
|
125
packages/tldraw/src/lib/shapes/shared/useEmojis.ts
Normal file
125
packages/tldraw/src/lib/shapes/shared/useEmojis.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useDialogs } from '../../ui/hooks/useDialogsProvider'
|
||||||
|
import EmojiDialog from './emojis'
|
||||||
|
import { EmojiDialogSingleton } from './emojis/EmojiDialog'
|
||||||
|
|
||||||
|
export function useEmojis(inputEl: HTMLTextAreaElement | null, onComplete: (text: string) => void) {
|
||||||
|
const { addDialog, removeDialog } = useDialogs()
|
||||||
|
const [emojiSearchText, setEmojiSearchText] = useState('')
|
||||||
|
const [isEmojiMenuOpen, setIsEmojiMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
setIsEmojiMenuOpen(false)
|
||||||
|
removeDialog('emoji')
|
||||||
|
setEmojiSearchText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEmojiSelect = (emoji: any) => {
|
||||||
|
if (!inputEl) return
|
||||||
|
|
||||||
|
const searchText = EmojiDialogSingleton?.component.refs.searchInput.current.value
|
||||||
|
inputEl.focus()
|
||||||
|
inputEl.setSelectionRange(
|
||||||
|
inputEl.selectionStart - searchText.length - 1,
|
||||||
|
inputEl.selectionStart
|
||||||
|
)
|
||||||
|
inputEl.setRangeText(emoji.native)
|
||||||
|
inputEl.setSelectionRange(inputEl.selectionStart + 1, inputEl.selectionStart + 1)
|
||||||
|
onComplete(inputEl.value)
|
||||||
|
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLTextAreaElement>,
|
||||||
|
coords: { top: number; left: number } | null
|
||||||
|
) => {
|
||||||
|
const emojiPicker = EmojiDialogSingleton?.component
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case ':': {
|
||||||
|
if (isEmojiMenuOpen) {
|
||||||
|
closeMenu()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
setEmojiSearchText('')
|
||||||
|
addDialog({
|
||||||
|
id: 'emoji',
|
||||||
|
component: EmojiDialog,
|
||||||
|
isCustomDialog: true,
|
||||||
|
dialogProps: {
|
||||||
|
onEmojiSelect,
|
||||||
|
onClickOutside: closeMenu,
|
||||||
|
top: coords?.top,
|
||||||
|
left: coords?.left,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setIsEmojiMenuOpen(true)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case ' ':
|
||||||
|
// fall-through
|
||||||
|
case 'Escape': {
|
||||||
|
if (isEmojiMenuOpen) {
|
||||||
|
closeMenu()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Enter':
|
||||||
|
case 'ArrowLeft':
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'ArrowUp':
|
||||||
|
case 'ArrowDown': {
|
||||||
|
if (isEmojiMenuOpen) {
|
||||||
|
emojiPicker.handleSearchKeyDown({
|
||||||
|
...e,
|
||||||
|
preventDefault: () => {
|
||||||
|
/* shim */
|
||||||
|
},
|
||||||
|
stopImmediatePropagation: () => {
|
||||||
|
/* shim */
|
||||||
|
},
|
||||||
|
})
|
||||||
|
e.preventDefault()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Backspace':
|
||||||
|
if (isEmojiMenuOpen) {
|
||||||
|
if (!emojiSearchText) {
|
||||||
|
closeMenu()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = emojiSearchText.slice(0, -1)
|
||||||
|
emojiPicker.refs.searchInput.current.value = text
|
||||||
|
emojiPicker.handleSearchInput()
|
||||||
|
setEmojiSearchText(text)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (isEmojiMenuOpen && e.key.length === 1 && e.key.match(/[a-zA-Z0-9]/)) {
|
||||||
|
emojiPicker.refs.searchInput.current.value = emojiSearchText + e.key
|
||||||
|
emojiPicker.handleSearchInput()
|
||||||
|
setEmojiSearchText(emojiSearchText + e.key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEmojiMenuOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { onKeyDown }
|
||||||
|
}
|
|
@ -3,7 +3,13 @@ import { useContainer } from '@tldraw/editor'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { TLUiDialog, useDialogs } from '../hooks/useDialogsProvider'
|
import { TLUiDialog, useDialogs } from '../hooks/useDialogsProvider'
|
||||||
|
|
||||||
const Dialog = ({ id, component: ModalContent, onClose }: TLUiDialog) => {
|
const Dialog = ({
|
||||||
|
id,
|
||||||
|
component: ModalContent,
|
||||||
|
onClose,
|
||||||
|
isCustomDialog,
|
||||||
|
dialogProps,
|
||||||
|
}: TLUiDialog) => {
|
||||||
const { removeDialog } = useDialogs()
|
const { removeDialog } = useDialogs()
|
||||||
|
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
|
@ -24,6 +30,10 @@ const Dialog = ({ id, component: ModalContent, onClose }: TLUiDialog) => {
|
||||||
[id, onClose, removeDialog]
|
[id, onClose, removeDialog]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isCustomDialog) {
|
||||||
|
return <ModalContent onClose={() => handleOpenChange(false)} {...dialogProps} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<_Dialog.Root onOpenChange={handleOpenChange} defaultOpen>
|
<_Dialog.Root onOpenChange={handleOpenChange} defaultOpen>
|
||||||
<_Dialog.Portal container={container}>
|
<_Dialog.Portal container={container}>
|
||||||
|
|
|
@ -12,6 +12,8 @@ export interface TLUiDialog {
|
||||||
id: string
|
id: string
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
component: (props: TLUiDialogProps) => any
|
component: (props: TLUiDialogProps) => any
|
||||||
|
isCustomDialog?: boolean
|
||||||
|
dialogProps?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -2608,6 +2608,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@emoji-mart/data@npm:^1.1.2":
|
||||||
|
version: 1.1.2
|
||||||
|
resolution: "@emoji-mart/data@npm:1.1.2"
|
||||||
|
checksum: c285aa159b00b728d37bc2c6a30ad007fd0d468982c80b16bc8ef6a982c615e9811d297a11ff485ee981bd9a41a63988ad34e8d4a65fb807c4a1bf74f3e92443
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@emotion/hash@npm:^0.9.0":
|
"@emotion/hash@npm:^0.9.0":
|
||||||
version: 0.9.1
|
version: 0.9.1
|
||||||
resolution: "@emotion/hash@npm:0.9.1"
|
resolution: "@emotion/hash@npm:0.9.1"
|
||||||
|
@ -7477,6 +7484,7 @@ __metadata:
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@tldraw/tldraw@workspace:packages/tldraw"
|
resolution: "@tldraw/tldraw@workspace:packages/tldraw"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@emoji-mart/data": "npm:^1.1.2"
|
||||||
"@peculiar/webcrypto": "npm:^1.4.0"
|
"@peculiar/webcrypto": "npm:^1.4.0"
|
||||||
"@radix-ui/react-alert-dialog": "npm:^1.0.5"
|
"@radix-ui/react-alert-dialog": "npm:^1.0.5"
|
||||||
"@radix-ui/react-context-menu": "npm:^2.1.5"
|
"@radix-ui/react-context-menu": "npm:^2.1.5"
|
||||||
|
@ -7495,6 +7503,7 @@ __metadata:
|
||||||
canvas-size: "npm:^1.2.6"
|
canvas-size: "npm:^1.2.6"
|
||||||
chokidar-cli: "npm:^3.0.0"
|
chokidar-cli: "npm:^3.0.0"
|
||||||
classnames: "npm:^2.3.2"
|
classnames: "npm:^2.3.2"
|
||||||
|
emoji-mart: "patch:emoji-mart@npm%3A5.5.2#~/.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch"
|
||||||
hotkeys-js: "npm:^3.11.2"
|
hotkeys-js: "npm:^3.11.2"
|
||||||
jest-canvas-mock: "npm:^2.5.2"
|
jest-canvas-mock: "npm:^2.5.2"
|
||||||
jest-environment-jsdom: "npm:^29.4.3"
|
jest-environment-jsdom: "npm:^29.4.3"
|
||||||
|
@ -12078,6 +12087,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"emoji-mart@npm:5.5.2":
|
||||||
|
version: 5.5.2
|
||||||
|
resolution: "emoji-mart@npm:5.5.2"
|
||||||
|
checksum: 3b891f7940b25fbe83eb784b655bca9fa1b552ddf1c76bf631d51c4b2adef7072834f58b08ed63dc3fccd635d63de7a38f40cd0320fa7c24d97b9ced60dec957
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"emoji-mart@patch:emoji-mart@npm%3A5.5.2#~/.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch":
|
||||||
|
version: 5.5.2
|
||||||
|
resolution: "emoji-mart@patch:emoji-mart@npm%3A5.5.2#~/.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch::version=5.5.2&hash=61a502"
|
||||||
|
checksum: 219b8f4b146f439564d40b0b25bb2090c307e7225890e585aea9d949be405d09526090d80d096fd1eebb778d43f4b354bf3c511ed6919d92780bed8735348d40
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"emoji-regex@npm:^10.3.0":
|
"emoji-regex@npm:^10.3.0":
|
||||||
version: 10.3.0
|
version: 10.3.0
|
||||||
resolution: "emoji-regex@npm:10.3.0"
|
resolution: "emoji-regex@npm:10.3.0"
|
||||||
|
|
Loading…
Reference in a new issue