feat(writekit): rename @robonen/editor to @robonen/writekit
Rename the rich-text editor package and all Editor* exports to Writekit*; remove the old vue/editor tree.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import type { Attrs, Node } from '../model';
|
||||
import {
|
||||
blockById,
|
||||
caret,
|
||||
inlineLength,
|
||||
isAcrossBlocks,
|
||||
isCollapsed,
|
||||
nextBlock,
|
||||
nodeInline,
|
||||
nodeSelection,
|
||||
orderedSelection,
|
||||
previousBlock,
|
||||
} from '../model';
|
||||
import type { Command, WritekitState } from '../state';
|
||||
import { createTransaction } from '../state';
|
||||
|
||||
/** Type/attrs for the block created when splitting `block` at `offset`. */
|
||||
function continuation(state: WritekitState, block: Node, offset: number): { type?: string; attrs?: Attrs } {
|
||||
const spec = state.schema.nodeSpec(block.type);
|
||||
|
||||
// Defining blocks (e.g. code-block) keep their identity across a split.
|
||||
if (spec?.defining)
|
||||
return { type: block.type, attrs: block.attrs };
|
||||
|
||||
// Pressing Enter at the end of a heading starts a fresh paragraph.
|
||||
if (block.type === 'heading' && offset >= inlineLength(nodeInline(block)) && state.registry.hasBlock('paragraph'))
|
||||
return { type: 'paragraph' };
|
||||
|
||||
// A new to-do item always starts unchecked (don't inherit the checked state).
|
||||
if (block.type === 'todo-list')
|
||||
return { type: block.type, attrs: { ...block.attrs, checked: false } };
|
||||
|
||||
// Otherwise continue the same type (lists keep their indent/listType attrs).
|
||||
return { type: block.type, attrs: block.attrs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the current text block at the caret (Enter). A non-collapsed same-block
|
||||
* selection is deleted first. Caret lands at the start of the new block.
|
||||
*/
|
||||
export const splitBlock: Command = (state, dispatch) => {
|
||||
const sel = state.selection;
|
||||
|
||||
if (sel.kind !== 'text' || isAcrossBlocks(sel))
|
||||
return false;
|
||||
|
||||
const block = blockById(state.doc, sel.focus.blockId);
|
||||
const spec = block && state.schema.nodeSpec(block.type);
|
||||
|
||||
if (!block || spec?.content.kind !== 'text')
|
||||
return false;
|
||||
|
||||
// Code blocks never split — Enter inserts a literal newline.
|
||||
if (spec.code) {
|
||||
if (dispatch) {
|
||||
const tr = createTransaction(state);
|
||||
let pos = sel.focus;
|
||||
|
||||
if (!isCollapsed(sel)) {
|
||||
const { from, to } = orderedSelection(sel, state.doc);
|
||||
tr.deleteText(block.id, from.offset, to.offset);
|
||||
pos = from;
|
||||
}
|
||||
|
||||
tr.insertText(pos, '\n', []).setSelection(caret(block.id, pos.offset + 1));
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
const tr = createTransaction(state);
|
||||
let pos = sel.focus;
|
||||
|
||||
if (!isCollapsed(sel)) {
|
||||
const { from, to } = orderedSelection(sel, state.doc);
|
||||
tr.deleteText(block.id, from.offset, to.offset);
|
||||
pos = from;
|
||||
}
|
||||
|
||||
const cont = continuation(state, block, pos.offset);
|
||||
tr.splitBlock(pos, cont.type, cont.attrs);
|
||||
tr.setSelection(caret(tr.lastSplitId!, 0));
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Insert a hard line break (Shift+Enter) inside the current block. */
|
||||
export const insertHardBreak: Command = (state, dispatch) => {
|
||||
const sel = state.selection;
|
||||
|
||||
if (sel.kind !== 'text' || !isCollapsed(sel))
|
||||
return false;
|
||||
|
||||
const block = blockById(state.doc, sel.focus.blockId);
|
||||
const spec = block && state.schema.nodeSpec(block.type);
|
||||
|
||||
if (!block || spec?.content.kind !== 'text')
|
||||
return false;
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(createTransaction(state)
|
||||
.insertText(sel.focus, '\n', state.storedMarks ?? [])
|
||||
.setSelection(caret(block.id, sel.focus.offset + 1)));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Backspace at the start of a block: merge it into the previous text block, or
|
||||
* select a preceding atom block (image/divider) so a second Backspace deletes it.
|
||||
*/
|
||||
export const joinBackward: Command = (state, dispatch) => {
|
||||
const sel = state.selection;
|
||||
|
||||
if (sel.kind !== 'text' || !isCollapsed(sel) || sel.focus.offset !== 0)
|
||||
return false;
|
||||
|
||||
const current = blockById(state.doc, sel.focus.blockId);
|
||||
const prev = previousBlock(state.doc, sel.focus.blockId);
|
||||
|
||||
if (!current || !prev)
|
||||
return false;
|
||||
|
||||
const currentSpec = state.schema.nodeSpec(current.type);
|
||||
const prevSpec = state.schema.nodeSpec(prev.type);
|
||||
|
||||
if (currentSpec?.isolating || prevSpec?.isolating)
|
||||
return false;
|
||||
|
||||
if (prevSpec?.content.kind !== 'text') {
|
||||
if (dispatch)
|
||||
dispatch(createTransaction(state).setSelection(nodeSelection([prev.id])));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentSpec?.content.kind !== 'text')
|
||||
return false;
|
||||
|
||||
if (dispatch) {
|
||||
const caretOffset = inlineLength(nodeInline(prev));
|
||||
dispatch(createTransaction(state).mergeBlock(current.id, prev.id).setSelection(caret(prev.id, caretOffset)));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Delete at the end of a block: merge the next text block into it. */
|
||||
export const joinForward: Command = (state, dispatch) => {
|
||||
const sel = state.selection;
|
||||
|
||||
if (sel.kind !== 'text' || !isCollapsed(sel))
|
||||
return false;
|
||||
|
||||
const current = blockById(state.doc, sel.focus.blockId);
|
||||
|
||||
if (!current || sel.focus.offset !== inlineLength(nodeInline(current)))
|
||||
return false;
|
||||
|
||||
const next = nextBlock(state.doc, current.id);
|
||||
if (!next)
|
||||
return false;
|
||||
|
||||
const currentSpec = state.schema.nodeSpec(current.type);
|
||||
const nextSpec = state.schema.nodeSpec(next.type);
|
||||
|
||||
if (currentSpec?.isolating || nextSpec?.isolating || currentSpec?.content.kind !== 'text' || nextSpec?.content.kind !== 'text')
|
||||
return false;
|
||||
|
||||
if (dispatch) {
|
||||
const caretOffset = inlineLength(nodeInline(current));
|
||||
dispatch(createTransaction(state).mergeBlock(next.id, current.id).setSelection(caret(current.id, caretOffset)));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
Reference in New Issue
Block a user