mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-21 13:51:59 +02:00
Fix selection handling with slow flush in mark placeholders on android, fix auto-capitalize after placeholder (#5084)
* Fix selection handling with slow flush in mark placeholders on android, fix auto-capitalize after placeholder * Add changeset * Correct typos
This commit is contained in:
parent
46d113fe1e
commit
50de780b1c
5
.changeset/friendly-oranges-remain.md
Normal file
5
.changeset/friendly-oranges-remain.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'slate-react': patch
|
||||
---
|
||||
|
||||
Fix selection handling with slow flush in mark placeholders on android, fix auto-capitalize after placeholder
|
@ -222,7 +222,7 @@ export const Editable = (props: EditableProps) => {
|
||||
if (range) {
|
||||
if (
|
||||
!ReactEditor.isComposing(editor) &&
|
||||
!androidInputManager?.hasPendingDiffs() &&
|
||||
!androidInputManager?.hasPendingChanges() &&
|
||||
!androidInputManager?.isFlushing()
|
||||
) {
|
||||
Transforms.select(editor, range)
|
||||
@ -784,11 +784,17 @@ export const Editable = (props: EditableProps) => {
|
||||
// before we receive the composition end event.
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (marks) {
|
||||
EDITOR_TO_PENDING_INSERTION_MARKS.set(editor, marks)
|
||||
} else {
|
||||
EDITOR_TO_PENDING_INSERTION_MARKS.delete(editor)
|
||||
const { selection } = editor
|
||||
if (selection) {
|
||||
const { anchor } = selection
|
||||
const { text, ...rest } = Node.leaf(editor, anchor.path)
|
||||
if (!Text.equals(rest as Text, marks as Text, { loose: true })) {
|
||||
EDITOR_TO_PENDING_INSERTION_MARKS.set(editor, marks)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
EDITOR_TO_PENDING_INSERTION_MARKS.delete(editor)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1616,7 +1622,14 @@ export type RenderPlaceholderProps = {
|
||||
export const DefaultPlaceholder = ({
|
||||
attributes,
|
||||
children,
|
||||
}: RenderPlaceholderProps) => <span {...attributes}>{children}</span>
|
||||
}: RenderPlaceholderProps) => (
|
||||
// COMPAT: Artificially add a line-break to the end on the placeholder element
|
||||
// to prevent Android IMEs to pick up its content in autocorrect and to auto-capitalize the first letter
|
||||
<span {...attributes}>
|
||||
{children}
|
||||
{IS_ANDROID && <br />}
|
||||
</span>
|
||||
)
|
||||
|
||||
/**
|
||||
* A default memoized decorate function.
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
IS_COMPOSING,
|
||||
} from '../../utils/weak-maps'
|
||||
|
||||
export type Action = { at: Point | Range; run: () => void }
|
||||
export type Action = { at?: Point | Range; run: () => void }
|
||||
|
||||
// https://github.com/facebook/draft-js/blob/main/src/component/handlers/composition/DraftEditorCompositionHandler.js#L41
|
||||
// When using keyboard English association function, conpositionEnd triggered too fast, resulting in after `insertText` still maintain association state.
|
||||
@ -48,6 +48,7 @@ export type AndroidInputManager = {
|
||||
|
||||
hasPendingDiffs: () => boolean
|
||||
hasPendingAction: () => boolean
|
||||
hasPendingChanges: () => boolean
|
||||
isFlushing: () => boolean | 'action'
|
||||
|
||||
handleUserSelect: (range: Range | null) => void
|
||||
@ -62,30 +63,6 @@ export type AndroidInputManager = {
|
||||
handleInput: () => void
|
||||
}
|
||||
|
||||
export function forceSwiftKeyUpdate(editor: ReactEditor) {
|
||||
const { document } = ReactEditor.getWindow(editor)
|
||||
debug('force ime update')
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.setAttribute('contenteditable', 'true')
|
||||
div.setAttribute('display', 'none')
|
||||
div.setAttribute('position', 'absolute')
|
||||
div.setAttribute('top', '0')
|
||||
div.setAttribute('left', '0')
|
||||
div.textContent = ' '
|
||||
|
||||
document.body.appendChild(div)
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(div)
|
||||
const selection = window.getSelection()
|
||||
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
div.parentElement?.removeChild(div)
|
||||
|
||||
ReactEditor.focus(editor)
|
||||
}
|
||||
|
||||
export function createAndroidInputManager({
|
||||
editor,
|
||||
scheduleOnDOMSelectionChange,
|
||||
@ -95,8 +72,9 @@ export function createAndroidInputManager({
|
||||
let compositionEndTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
let flushTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
let actionTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
let idCounter = 0
|
||||
let isInsertAfterMarkPlaceholder = false
|
||||
let insertPositionHint: StringDiff | null | false = false
|
||||
|
||||
const applyPendingSelection = () => {
|
||||
const pendingSelection = EDITOR_TO_PENDING_SELECTION.get(editor)
|
||||
@ -121,17 +99,19 @@ export function createAndroidInputManager({
|
||||
return
|
||||
}
|
||||
|
||||
const target = Point.isPoint(action.at)
|
||||
? normalizePoint(editor, action.at)
|
||||
: normalizeRange(editor, action.at)
|
||||
if (action.at) {
|
||||
const target = Point.isPoint(action.at)
|
||||
? normalizePoint(editor, action.at)
|
||||
: normalizeRange(editor, action.at)
|
||||
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetRange = Editor.range(editor, target)
|
||||
if (!editor.selection || !Range.equals(editor.selection, targetRange)) {
|
||||
Transforms.select(editor, target)
|
||||
const targetRange = Editor.range(editor, target)
|
||||
if (!editor.selection || !Range.equals(editor.selection, targetRange)) {
|
||||
Transforms.select(editor, target)
|
||||
}
|
||||
}
|
||||
|
||||
action.run()
|
||||
@ -142,6 +122,7 @@ export function createAndroidInputManager({
|
||||
clearTimeout(flushTimeoutId)
|
||||
flushTimeoutId = null
|
||||
}
|
||||
|
||||
if (actionTimeoutId) {
|
||||
clearTimeout(actionTimeoutId)
|
||||
actionTimeoutId = null
|
||||
@ -156,6 +137,7 @@ export function createAndroidInputManager({
|
||||
flushing = true
|
||||
setTimeout(() => (flushing = false))
|
||||
}
|
||||
|
||||
if (hasPendingAction()) {
|
||||
flushing = 'action'
|
||||
}
|
||||
@ -182,8 +164,9 @@ export function createAndroidInputManager({
|
||||
editor.marks = pendingMarks
|
||||
}
|
||||
|
||||
if (pendingMarks) {
|
||||
isInsertAfterMarkPlaceholder = true
|
||||
if (pendingMarks && insertPositionHint === false) {
|
||||
insertPositionHint = null
|
||||
debug('insert after mark placeholder')
|
||||
}
|
||||
|
||||
const range = targetRange(diff)
|
||||
@ -225,6 +208,7 @@ export function createAndroidInputManager({
|
||||
const selection = selectionRef?.unref()
|
||||
if (
|
||||
selection &&
|
||||
!EDITOR_TO_PENDING_SELECTION.get(editor) &&
|
||||
(!editor.selection || !Range.equals(selection, editor.selection))
|
||||
) {
|
||||
Transforms.select(editor, selection)
|
||||
@ -252,6 +236,7 @@ export function createAndroidInputManager({
|
||||
EDITOR_TO_USER_MARKS.delete(editor)
|
||||
if (userMarks !== undefined) {
|
||||
editor.marks = userMarks
|
||||
editor.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,7 +311,11 @@ export function createAndroidInputManager({
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleAction = (at: Point | Range, run: () => void): void => {
|
||||
const scheduleAction = (
|
||||
run: () => void,
|
||||
{ at }: { at?: Point | Range } = {}
|
||||
): void => {
|
||||
insertPositionHint = false
|
||||
debug('scheduleAction', { at, run })
|
||||
|
||||
EDITOR_TO_PENDING_SELECTION.delete(editor)
|
||||
@ -355,6 +344,14 @@ export function createAndroidInputManager({
|
||||
let targetRange: Range | null = null
|
||||
const data = (event as any).dataTransfer || event.data || undefined
|
||||
|
||||
if (
|
||||
insertPositionHint !== false &&
|
||||
type !== 'insertText' &&
|
||||
type !== 'insertCompositionText'
|
||||
) {
|
||||
insertPositionHint = false
|
||||
}
|
||||
|
||||
let [nativeTargetRange] = (event as any).getTargetRanges()
|
||||
if (nativeTargetRange) {
|
||||
targetRange = ReactEditor.toSlateRange(editor, nativeTargetRange, {
|
||||
@ -403,8 +400,9 @@ export function createAndroidInputManager({
|
||||
}
|
||||
|
||||
const direction = type.endsWith('Backward') ? 'backward' : 'forward'
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.deleteFragment(editor, { direction })
|
||||
return scheduleAction(
|
||||
() => Editor.deleteFragment(editor, { direction }),
|
||||
{ at: targetRange }
|
||||
)
|
||||
}
|
||||
|
||||
@ -412,7 +410,9 @@ export function createAndroidInputManager({
|
||||
case 'deleteByComposition':
|
||||
case 'deleteByCut':
|
||||
case 'deleteByDrag': {
|
||||
return scheduleAction(targetRange, () => Editor.deleteFragment(editor))
|
||||
return scheduleAction(() => Editor.deleteFragment(editor), {
|
||||
at: targetRange,
|
||||
})
|
||||
}
|
||||
|
||||
case 'deleteContent':
|
||||
@ -430,7 +430,9 @@ export function createAndroidInputManager({
|
||||
}
|
||||
}
|
||||
|
||||
return scheduleAction(targetRange, () => Editor.deleteForward(editor))
|
||||
return scheduleAction(() => Editor.deleteForward(editor), {
|
||||
at: targetRange,
|
||||
})
|
||||
}
|
||||
|
||||
case 'deleteContentBackward': {
|
||||
@ -455,58 +457,73 @@ export function createAndroidInputManager({
|
||||
})
|
||||
}
|
||||
|
||||
return scheduleAction(targetRange, () => Editor.deleteBackward(editor))
|
||||
}
|
||||
|
||||
case 'deleteEntireSoftLine': {
|
||||
return scheduleAction(targetRange, () => {
|
||||
Editor.deleteBackward(editor, { unit: 'line' })
|
||||
Editor.deleteForward(editor, { unit: 'line' })
|
||||
return scheduleAction(() => Editor.deleteBackward(editor), {
|
||||
at: targetRange,
|
||||
})
|
||||
}
|
||||
|
||||
case 'deleteEntireSoftLine': {
|
||||
return scheduleAction(
|
||||
() => {
|
||||
Editor.deleteBackward(editor, { unit: 'line' })
|
||||
Editor.deleteForward(editor, { unit: 'line' })
|
||||
},
|
||||
{ at: targetRange }
|
||||
)
|
||||
}
|
||||
|
||||
case 'deleteHardLineBackward': {
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.deleteBackward(editor, { unit: 'block' })
|
||||
return scheduleAction(
|
||||
() => Editor.deleteBackward(editor, { unit: 'block' }),
|
||||
{ at: targetRange }
|
||||
)
|
||||
}
|
||||
|
||||
case 'deleteSoftLineBackward': {
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.deleteBackward(editor, { unit: 'line' })
|
||||
return scheduleAction(
|
||||
() => Editor.deleteBackward(editor, { unit: 'line' }),
|
||||
{ at: targetRange }
|
||||
)
|
||||
}
|
||||
|
||||
case 'deleteHardLineForward': {
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.deleteForward(editor, { unit: 'block' })
|
||||
return scheduleAction(
|
||||
() => Editor.deleteForward(editor, { unit: 'block' }),
|
||||
{ at: targetRange }
|
||||
)
|
||||
}
|
||||
|
||||
case 'deleteSoftLineForward': {
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.deleteForward(editor, { unit: 'line' })
|
||||
return scheduleAction(
|
||||
() => Editor.deleteForward(editor, { unit: 'line' }),
|
||||
{ at: targetRange }
|
||||
)
|
||||
}
|
||||
|
||||
case 'deleteWordBackward': {
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.deleteBackward(editor, { unit: 'word' })
|
||||
return scheduleAction(
|
||||
() => Editor.deleteBackward(editor, { unit: 'word' }),
|
||||
{ at: targetRange }
|
||||
)
|
||||
}
|
||||
|
||||
case 'deleteWordForward': {
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.deleteForward(editor, { unit: 'word' })
|
||||
return scheduleAction(
|
||||
() => Editor.deleteForward(editor, { unit: 'word' }),
|
||||
{ at: targetRange }
|
||||
)
|
||||
}
|
||||
|
||||
case 'insertLineBreak': {
|
||||
return scheduleAction(targetRange, () => Editor.insertSoftBreak(editor))
|
||||
return scheduleAction(() => Editor.insertSoftBreak(editor), {
|
||||
at: targetRange,
|
||||
})
|
||||
}
|
||||
|
||||
case 'insertParagraph': {
|
||||
return scheduleAction(targetRange, () => Editor.insertBreak(editor))
|
||||
return scheduleAction(() => Editor.insertBreak(editor), {
|
||||
at: targetRange,
|
||||
})
|
||||
}
|
||||
case 'insertCompositionText':
|
||||
case 'deleteCompositionText':
|
||||
@ -517,15 +534,15 @@ export function createAndroidInputManager({
|
||||
case 'insertReplacementText':
|
||||
case 'insertText': {
|
||||
if (data?.constructor.name === 'DataTransfer') {
|
||||
return scheduleAction(targetRange, () =>
|
||||
ReactEditor.insertData(editor, data)
|
||||
)
|
||||
return scheduleAction(() => ReactEditor.insertData(editor, data), {
|
||||
at: targetRange,
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof data === 'string' && data.includes('\n')) {
|
||||
return scheduleAction(Range.end(targetRange), () =>
|
||||
Editor.insertSoftBreak(editor)
|
||||
)
|
||||
return scheduleAction(() => Editor.insertSoftBreak(editor), {
|
||||
at: Range.end(targetRange),
|
||||
})
|
||||
}
|
||||
|
||||
let text = data ?? ''
|
||||
@ -537,41 +554,80 @@ export function createAndroidInputManager({
|
||||
}
|
||||
|
||||
if (Path.equals(targetRange.anchor.path, targetRange.focus.path)) {
|
||||
// COMPAT: Swiftkey has a weird bug where the target range of the 2nd word
|
||||
// inserted after a mark placeholder is inserted with a anchor offset off by 1.
|
||||
// So writing 'some text' will result in 'some ttext'. If we force a IME update
|
||||
// after inserting the first word, swiftkey will insert with the correct offset
|
||||
if (text.endsWith(' ') && isInsertAfterMarkPlaceholder) {
|
||||
isInsertAfterMarkPlaceholder = false
|
||||
forceSwiftKeyUpdate(editor)
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.insertText(editor, text)
|
||||
)
|
||||
}
|
||||
|
||||
const [start, end] = Range.edges(targetRange)
|
||||
return storeDiff(start.path, {
|
||||
|
||||
const diff = {
|
||||
start: start.offset,
|
||||
end: end.offset,
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
// COMPAT: Swiftkey has a weird bug where the target range of the 2nd word
|
||||
// inserted after a mark placeholder is inserted with an anchor offset off by 1.
|
||||
// So writing 'some text' will result in 'some ttext'. Luckily all 'normal' insert
|
||||
// text events are fired with the correct target ranges, only the final 'insertComposition'
|
||||
// isn't, so we can adjust the target range start offset if we are confident this is the
|
||||
// swiftkey insert causing the issue.
|
||||
if (text && insertPositionHint && type === 'insertCompositionText') {
|
||||
const hintPosition =
|
||||
insertPositionHint.start + insertPositionHint.text.search(/\S|$/)
|
||||
const diffPosition = diff.start + diff.text.search(/\S|$/)
|
||||
|
||||
if (
|
||||
diffPosition === hintPosition + 1 &&
|
||||
diff.end ===
|
||||
insertPositionHint.start + insertPositionHint.text.length
|
||||
) {
|
||||
debug('adjusting swiftKey insert position using hint')
|
||||
diff.start -= 1
|
||||
insertPositionHint = null
|
||||
scheduleFlush()
|
||||
} else {
|
||||
insertPositionHint = false
|
||||
}
|
||||
} else if (type === 'insertText') {
|
||||
if (insertPositionHint === null) {
|
||||
insertPositionHint = diff
|
||||
} else if (
|
||||
insertPositionHint &&
|
||||
Range.isCollapsed(targetRange) &&
|
||||
insertPositionHint.end + insertPositionHint.text.length ===
|
||||
start.offset
|
||||
) {
|
||||
insertPositionHint = {
|
||||
...insertPositionHint,
|
||||
text: insertPositionHint.text + text,
|
||||
}
|
||||
} else {
|
||||
insertPositionHint = false
|
||||
}
|
||||
} else {
|
||||
insertPositionHint = false
|
||||
}
|
||||
|
||||
storeDiff(start.path, diff)
|
||||
return
|
||||
}
|
||||
|
||||
return scheduleAction(targetRange, () =>
|
||||
Editor.insertText(editor, text)
|
||||
)
|
||||
return scheduleAction(() => Editor.insertText(editor, text), {
|
||||
at: targetRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasPendingAction = () => {
|
||||
return !!EDITOR_TO_PENDING_ACTION.get(editor) || !!actionTimeoutId
|
||||
return !!EDITOR_TO_PENDING_ACTION.get(editor)
|
||||
}
|
||||
|
||||
const hasPendingDiffs = () => {
|
||||
return !!EDITOR_TO_PENDING_DIFFS.get(editor)?.length
|
||||
}
|
||||
|
||||
const hasPendingChanges = () => {
|
||||
return hasPendingAction() || hasPendingDiffs()
|
||||
}
|
||||
|
||||
const isFlushing = () => {
|
||||
return flushing
|
||||
}
|
||||
@ -584,13 +640,22 @@ export function createAndroidInputManager({
|
||||
flushTimeoutId = null
|
||||
}
|
||||
|
||||
const pathChanged =
|
||||
range &&
|
||||
(!editor.selection ||
|
||||
!Path.equals(editor.selection.anchor.path, range?.anchor.path))
|
||||
const { selection } = editor
|
||||
if (!range) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pathChanged) {
|
||||
isInsertAfterMarkPlaceholder = false
|
||||
const pathChanged =
|
||||
!selection || !Path.equals(selection.anchor.path, range.anchor.path)
|
||||
const parentPathChanged =
|
||||
!selection ||
|
||||
!Path.equals(
|
||||
selection.anchor.path.slice(0, -1),
|
||||
range.anchor.path.slice(0, -1)
|
||||
)
|
||||
|
||||
if ((pathChanged && insertPositionHint) || parentPathChanged) {
|
||||
insertPositionHint = false
|
||||
}
|
||||
|
||||
if (pathChanged || !hasPendingDiffs()) {
|
||||
@ -643,6 +708,8 @@ export function createAndroidInputManager({
|
||||
|
||||
hasPendingDiffs,
|
||||
hasPendingAction,
|
||||
hasPendingChanges,
|
||||
|
||||
isFlushing,
|
||||
|
||||
handleUserSelect,
|
||||
|
@ -124,8 +124,9 @@ export const withReact = <T extends Editor>(editor: T) => {
|
||||
transformPendingRange(editor, pendingSelection, op)
|
||||
)
|
||||
}
|
||||
|
||||
const pendingAction = EDITOR_TO_PENDING_ACTION.get(editor)
|
||||
if (pendingAction) {
|
||||
if (pendingAction?.at) {
|
||||
const at = Point.isPoint(pendingAction?.at)
|
||||
? transformPendingPoint(editor, pendingAction.at, op)
|
||||
: transformPendingRange(editor, pendingAction.at, op)
|
||||
|
Loading…
x
Reference in New Issue
Block a user