Skip to content

Commit

Permalink
refactor(portable-text-editor): improve placeholder block handling
Browse files Browse the repository at this point in the history
This will fix som bugs, and simplify logic regarding how placeholder blocks are inserted and genarated patches for
  • Loading branch information
skogsmaskin authored and rexxars committed Sep 23, 2022
1 parent fb9fbe2 commit 1c5e976
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ export class PortableTextEditor extends React.Component<
}
// Set the new value
debug('Replacing changed nodes')
if (this.props.value) {
if (val && val.length > 0) {
const slateValueFromProps = toSlateValue(
val,
{
Expand All @@ -356,8 +356,6 @@ export class PortableTextEditor extends React.Component<
KEY_TO_SLATE_ELEMENT.get(this.slateInstance)
)
this.slateInstance.children = slateValueFromProps
} else {
this.slateInstance.children = [this.slateInstance.createPlaceholderBlock()]
}
callbackFn()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,23 +169,15 @@ export function createWithPatches(
return editor
}

// Make sure the actual value is an array, and then insert the placeholder block into it
// before we produce any other patches that will target that block.
if (editorWasEmpty && operation.type !== 'set_selection') {
patches.push(setIfMissing([], []))
patches.push(
insert(
[
fromSlateValue(
previousChildren.length === 0
? [editor.createPlaceholderBlock()]
: previousChildren,
portableTextFeatures.types.block.name,
KEY_TO_VALUE_ELEMENT.get(editor)
)[0],
],
'before',
[0]
previousChildren.forEach((c, index) => {
patches.push(
insert(fromSlateValue([c], portableTextFeatures.types.block.name), 'after', [index])
)
)
})
}
switch (operation.type) {
case 'insert_text':
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {Transforms, Descendant} from 'slate'
import {PortableTextSlateEditor} from '../../types/editor'
import {PortableTextFeatures} from '../../types/portableText'
import {debugWithName} from '../../utils/debug'
import {withoutPatching} from '../../utils/withoutPatching'
import {withoutSaving} from './createWithUndoRedo'

const debug = debugWithName('plugin:withPlaceholderBlock')

interface Options {
portableTextFeatures: PortableTextFeatures
keyGenerator: () => string
}
/**
* Keep a "placeholder" block present when the editor is empty
*
*/
export function createWithPlaceholderBlock({
portableTextFeatures,
keyGenerator,
}: Options): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
return function withPlaceholderBlock(editor: PortableTextSlateEditor): PortableTextSlateEditor {
editor.createPlaceholderBlock = (): Descendant => {
return {
_type: portableTextFeatures.types.block.name,
_key: keyGenerator(),
style: portableTextFeatures.styles[0].value,
markDefs: [],
children: [
{
_type: 'span',
_key: keyGenerator(),
text: '',
marks: [],
},
],
}
}
const {onChange} = editor
// Make sure there's a placeholder block present if the editor's children become empty
editor.onChange = () => {
onChange()
if (editor.children.length === 0) {
withoutPatching(editor, () => {
withoutSaving(editor, () => {
debug('Inserting placeholder block')
Transforms.deselect(editor)
Transforms.insertNodes(editor, editor.createPlaceholderBlock(), {
at: [0],
})
Transforms.select(editor, {
focus: {path: [0, 0], offset: 0},
anchor: {path: [0, 0], offset: 0},
})
editor.onChange()
})
})
}
}
return editor
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,6 @@ interface Options {
*/
export function createWithUtils({portableTextFeatures, keyGenerator}: Options) {
return function withUtils(editor: PortableTextSlateEditor): PortableTextSlateEditor {
editor.createPlaceholderBlock = (): Descendant => {
return {
_type: portableTextFeatures.types.block.name,
_key: keyGenerator(),
style: portableTextFeatures.styles[0].value,
markDefs: [],
children: [
{
_type: 'span',
_key: keyGenerator(),
text: '',
marks: [],
},
],
}
}
// Expands the the selection to wrap around the word the focus is at
editor.pteExpandToWord = () => {
const {selection} = editor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {createOperationToPatches} from '../../utils/operationToPatches'
import {createWithMaxBlocks} from './createWithMaxBlocks'
import {createWithObjectKeys} from './createWithObjectKeys'
import {createWithPatches} from './createWithPatches'
import {createWithPlaceholderBlock} from './createWithPlaceholderBlock'
import {createWithPortableTextBlockStyle} from './createWithPortableTextBlockStyle'
import {createWithPortableTextLists} from './createWithPortableTextLists'
import {createWithPortableTextMarkModel} from './createWithPortableTextMarkModel'
Expand Down Expand Up @@ -57,6 +58,7 @@ export const withPlugins = <T extends Editor>(
keyGenerator
)
const withPortableTextBlockStyle = createWithPortableTextBlockStyle(portableTextFeatures, change$)
const withPlaceholderBlock = createWithPlaceholderBlock({keyGenerator, portableTextFeatures})
const withUtils = createWithUtils({keyGenerator, portableTextFeatures})
const withPortableTextSelections = createWithPortableTextSelections(change$, portableTextFeatures)
e.destroy = () => {
Expand All @@ -69,7 +71,9 @@ export const withPlugins = <T extends Editor>(
}
const minimal = withSchemaTypes(
withObjectKeys(
withPortableTextMarkModel(withPortableTextBlockStyle(withUtils(withPortableTextLists(e))))
withPortableTextMarkModel(
withPortableTextBlockStyle(withUtils(withPlaceholderBlock(withPortableTextLists(e))))
)
)
)
const full =
Expand All @@ -80,7 +84,9 @@ export const withPlugins = <T extends Editor>(
withPortableTextMarkModel(
withPortableTextBlockStyle(
withPortableTextLists(
withUtils(withMaxBlocks(withUndoRedo(withPatches(withPortableTextSelections(e)))))
withPlaceholderBlock(
withUtils(withMaxBlocks(withUndoRedo(withPatches(withPortableTextSelections(e)))))
)
)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable max-statements */
import {Editor, Transforms, Element, Path as SlatePath, Descendant} from 'slate'
import {Editor, Transforms, Element, Path as SlatePath, Descendant, Text, Node} from 'slate'
import * as DMP from 'diff-match-patch'
import {Path, KeyedSegment, PathSegment} from '@sanity/types'
import {isEqual} from 'lodash'
import type {Patch, InsertPatch, UnsetPatch, SetPatch, DiffMatchPatch} from '../types/patch'
import {PortableTextFeatures, PortableTextBlock, PortableTextChild} from '../types/portableText'
import {applyAll} from '../patch/applyPatch'
import {isEqualToEmptyEditor, toSlateValue} from './values'
import {toSlateValue} from './values'
import {debugWithName} from './debug'
import {KEY_TO_SLATE_ELEMENT} from './weakMaps'

Expand All @@ -30,12 +30,12 @@ export function createPatchToOperations(
return blockKey ? node._key === blockKey : indx === patch.path[0]
})
const block = editor.children[blockIndex] as Element
const childKey = findLastKey([patch.path[2]])
const childIndex = block.children.findIndex((node, indx) => {
return childKey ? node._key === childKey : indx === patch.path[0]
})
const parsed = dmp.patch_fromText(patch.value)[0]
if (parsed) {
if (parsed && editor.isTextBlock(block)) {
const childKey = findLastKey([patch.path[2]])
const childIndex = block.children.findIndex((node, indx) => {
return childKey ? node._key === childKey : indx === patch.path[0]
})
const slatePath = [blockIndex, childIndex]
const distance = parsed.length2 - parsed.length1
const point = {
Expand Down Expand Up @@ -75,8 +75,9 @@ export function createPatchToOperations(
})
}
debugState(editor, 'after')
return true
}
return true
return false
}

function insertPatch(editor: Editor, patch: InsertPatch) {
Expand All @@ -95,19 +96,7 @@ export function createPatchToOperations(
const normalizedIdx = position === 'after' ? index + 1 : index
debug(`Inserting blocks at path [${normalizedIdx}]`)
debugState(editor, 'before')
const isEmpty = isEqualToEmptyEditor(editor.children, portableTextFeatures)
debug('isEmpty', isEmpty)
if (isEmpty) {
debug('Removing placeholder block')
Transforms.removeNodes(editor, {at: [0]})
}
Transforms.insertNodes(editor, blocksToInsert, {at: [normalizedIdx]})
if (isEmpty) {
Transforms.select(editor, {
focus: {path: [0, 0], offset: 0},
anchor: {path: [0, 0], offset: 0},
})
}
debugState(editor, 'after')
return true
}
Expand Down Expand Up @@ -162,7 +151,7 @@ export function createPatchToOperations(
? node._key === patch.path[2]._key
: indx === patch.path[2]
})
let value: any = patch.value
let value = patch.value
const targetPath: SlatePath = childIndex > -1 ? [blockIndex, childIndex] : [blockIndex]
if (typeof patch.path[3] === 'string') {
value = {}
Expand All @@ -175,7 +164,7 @@ export function createPatchToOperations(
debugState(editor, 'before')
if (targetPath.length === 1) {
debug('Setting block property')
const {children, ...nextRest} = value
const {children, ...nextRest} = value as PortableTextBlock
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {children: prevChildren, ...prevRest} = block || {children: undefined}
editor.apply({
Expand All @@ -202,7 +191,7 @@ export function createPatchToOperations(
})
})
}
} else if (typeof value.text === 'string') {
} else if (Text.isText(value)) {
debug('Setting text property')
const prevSel = editor.selection && {...editor.selection}
editor.apply({
Expand Down Expand Up @@ -238,7 +227,7 @@ export function createPatchToOperations(
type: 'set_node',
path: targetPath,
properties: {},
newProperties: value,
newProperties: value as Partial<Node>,
})
}
debugState(editor, 'after')
Expand All @@ -258,14 +247,10 @@ export function createPatchToOperations(
if (patch.path.length === 0) {
debug(`Removing everything`)
debugState(editor, 'before')
Transforms.deselect(editor)
editor.children.forEach((c, i) => {
Transforms.removeNodes(editor, {at: [i]})
})
Transforms.insertNodes(editor, editor.createPlaceholderBlock(), {at: [0]})
Transforms.select(editor, {
focus: {path: [0, 0], offset: 0},
anchor: {path: [0, 0], offset: 0},
})
debugState(editor, 'after')
return true
}
Expand All @@ -275,18 +260,20 @@ export function createPatchToOperations(
const index = editor.children.findIndex((node, indx) =>
lastKey ? node._key === lastKey : indx === patch.path[0]
)
if (index > -1) {
debug(`Removing block at path [${index}]`)
debugState(editor, 'before')
if (editor.selection && editor.selection.focus.path[0] === index) {
const point = {path: [editor.selection.focus.path[0] - 1 || 0], offset: 0}
Transforms.select(editor, {focus: point, anchor: point})
Transforms.move(editor, {unit: 'line'})
}
Transforms.removeNodes(editor, {at: [index]})
debugState(editor, 'after')
return true
debug(`Removing block at path [${index}]`)
debugState(editor, 'before')
if (
editor.selection &&
editor.selection.focus.path[0] === index &&
editor.children[index - 1]
) {
const point = {path: [Math.max(editor.selection.focus.path[0] - 1, 0)], offset: 0}
Transforms.select(editor, {focus: point, anchor: point})
Transforms.move(editor, {unit: 'line'})
}
Transforms.removeNodes(editor, {at: [index]})
debugState(editor, 'after')
return true
}

const blockIndex = editor.children.findIndex((node, indx) => {
Expand Down Expand Up @@ -329,9 +316,9 @@ export function createPatchToOperations(
if (isMergeUnset) {
debug('Adjusting selection for merging of nodes')
prevSel.focus = {...prevSel.focus}
prevSel.focus.path = [targetPath[0], targetPath[1] - 1]
prevSel.focus.path = [targetPath[0], Math.max(targetPath[1] - 1, 0)]
prevSel.focus.offset =
block.children[childIndex - 1].text.length -
block.children[Math.max(childIndex - 1, 0)].text.length -
block.children[childIndex].text.length +
prevSel.focus.offset
prevSel.anchor = prevSel.focus
Expand Down

0 comments on commit 1c5e976

Please sign in to comment.