slate#Point TypeScript Examples
The following examples show how to use
slate#Point.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: on-code-block-keydown.ts From fantasy-editor with MIT License | 6 votes |
onKeyDownCodeBlock = (e: KeyboardEvent, editor: Editor) => {
if (isHotkey('Enter', e)) {
const match = Editor.above(editor, {
match: n => n.type === BLOCK_CODE,
});
if (match) {
const [, path] = match;
const text = Editor.string(editor, path);
e.preventDefault();
const {selection} = editor;
if(selection && Range.isCollapsed(selection)){
const start = Editor.end(editor, path);
if(text.endsWith('\n')&&Point.equals(selection.anchor, start)){
Transforms.delete(editor, {
at: {
anchor: {
offset: selection.anchor.offset-1,
path: selection.anchor.path
},
focus: selection.focus
}
})
let nextPath = Path.next(path);
Transforms.insertNodes(editor, {
type: BLOCK_PARAGRAPH,
children: [{
text: ''
}]
}, {
at: nextPath
});
Transforms.select(editor, nextPath);
return;
}
}
editor.insertText('\n');
}
}
}
Example #2
Source File: use-mention.tsx From fantasy-editor with MIT License | 6 votes |
isWordAfterTrigger = (editor: Editor, { at, trigger }: { at: Point; trigger: string }) => {
// Point at the start of previous word (excluding punctuation)
const wordBefore = Editor.before(editor, at, { unit: 'word' });
// Point before wordBefore
const before = wordBefore && Editor.before(editor, wordBefore);
// Range from before to start
const beforeRange = before && Editor.range(editor, before, at);
// Before text
const beforeText = beforeRange && Editor.string(editor, beforeRange);
// Starts with char and ends with word characters
const escapedTrigger = escapeRegExp(trigger);
const beforeRegex = new RegExp(`^${escapedTrigger}(\\w+)$`);
// Match regex on before text
const match = beforeText && beforeText.match(beforeRegex);
return {
range: beforeRange,
match,
};
}
Example #3
Source File: use-mention.tsx From fantasy-editor with MIT License | 6 votes |
isPointAtWordEnd = (editor: Editor, { at }: { at: Point }) => {
// Point after at
const after = Editor.after(editor, at);
// From at to after
const afterRange = Editor.range(editor, at, after);
const afterText = Editor.string(editor, afterRange);
// Match regex on after text
return !!afterText.match(AFTER_MATCH_REGEX);
}
Example #4
Source File: with-table.ts From fantasy-editor with MIT License | 6 votes |
withTable = () => <T extends Editor>(editor:T) => {
const {deleteBackward, deleteForward} = editor;
const preventDeleteCell = (operation: any, pointCallback: any) => (
unit: any
) => {
const { selection } = editor;
if (selection && Range.isCollapsed(selection)) {
const [cell] = Editor.nodes(editor, {
match: (n) => n.type === BLOCK_TABLE_CELL,
});
if (cell) {
const [, cellPath] = cell;
const start = pointCallback(editor, cellPath);
if (Point.equals(selection.anchor, start)) {
return;
}
}
}
operation(unit);
};
editor.deleteBackward = preventDeleteCell(deleteBackward, Editor.start);
editor.deleteForward = preventDeleteCell(deleteForward, Editor.end);
return editor;
}
Example #5
Source File: LastFocusedNode.tsx From react-editor-kit with MIT License | 6 votes |
useLastFocused = (editor: ReactEditor) => {
const [state, setState] = useState<LastFocusedState>({
element: undefined,
point: undefined,
selection: undefined,
});
const current = getActiveNodeType(editor);
useEffect(() => {
if (!ReactEditor.isFocused(editor)) {
return;
}
const { selection } = editor;
if (!selection) {
return;
}
if (current) {
const point = selection.focus;
const [element] = Editor.parent(editor, point);
if (
state.element === element &&
state.point &&
Point.equals(state.point, point)
) {
return;
}
setState({
element: element,
point,
selection: clone(selection),
});
}
}, [current, state, editor.operations]);
return state;
}
Example #6
Source File: markdown-shortcuts.component.ts From slate-angular with MIT License | 5 votes |
withShortcuts = editor => {
const { deleteBackward, insertText } = editor;
editor.insertText = text => {
const { selection } = editor;
if (text === ' ' && selection && Range.isCollapsed(selection)) {
const { anchor } = selection;
const block = Editor.above<Element>(editor, {
match: n => Editor.isBlock(editor, n)
});
const path = block ? block[1] : [];
const start = Editor.start(editor, path);
const range = { anchor, focus: start };
const beforeText = Editor.string(editor, range);
const type = SHORTCUTS[beforeText];
if (type) {
Transforms.select(editor, range);
Transforms.delete(editor);
Transforms.setNodes(editor, { type }, { match: n => Editor.isBlock(editor, n) });
if (type === 'list-item') {
const list: BulletedListElement = { type: 'bulleted-list', children: [] };
Transforms.wrapNodes<Element>(editor, list, {
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'list-item'
});
}
return;
}
}
insertText(text);
};
editor.deleteBackward = (...args) => {
const { selection } = editor;
if (selection && Range.isCollapsed(selection)) {
const match = Editor.above<Element>(editor, {
match: n => Editor.isBlock(editor, n)
});
if (match) {
const [block, path] = match;
const start = Editor.start(editor, path);
if (block.type !== 'paragraph' && Point.equals(selection.anchor, start)) {
Transforms.setNodes(editor, { type: 'paragraph' });
if (block.type === 'list-item') {
Transforms.unwrapNodes(editor, {
match: n => Element.isElement(n) && n.type === 'bulleted-list',
split: true
});
}
return;
}
}
deleteBackward(...args);
}
};
return editor;
}
Example #7
Source File: vue-editor.ts From slate-vue with MIT License | 4 votes |
VueEditor = {
/**
* Find a key for a Slate node.
*/
findKey(editor: VueEditor, node: Node): Key {
let key = NODE_TO_KEY.get(node)
if (!key) {
key = new Key()
NODE_TO_KEY.set(node, key)
}
return key
},
/**
* Find the path of Slate node.
*/
findPath(editor: VueEditor, node: Node): Path {
const path: Path = []
let child = node
while (true) {
const parent = NODE_TO_PARENT.get(child)
if (parent == null) {
if (Editor.isEditor(child)) {
return path
} else {
break
}
}
const i = NODE_TO_INDEX.get(child)
if (i == null) {
break
}
path.unshift(i)
child = parent
}
throw new Error(
`Unable to find the path for Slate node: ${JSON.stringify(node)}`
)
},
/**
* Check if the editor is focused.
*/
isFocused(editor: VueEditor): boolean {
return !!IS_FOCUSED.get(editor)
},
/**
* Check if the editor is in read-only mode.
*/
isReadOnly(editor: VueEditor): boolean {
return !!IS_READ_ONLY.get(editor)
},
/**
* Blur the editor.
*/
blur(editor: VueEditor): void {
const el = VueEditor.toDOMNode(editor, editor)
IS_FOCUSED.set(editor, false)
if (window.document.activeElement === el) {
el.blur()
}
},
/**
* Focus the editor.
*/
focus(editor: VueEditor): void {
const el = VueEditor.toDOMNode(editor, editor)
IS_FOCUSED.set(editor, true)
if (window.document.activeElement !== el) {
el.focus({ preventScroll: true })
}
},
/**
* Deselect the editor.
*/
deselect(editor: VueEditor): void {
const { selection } = editor
const domSelection = window.getSelection()
if (domSelection && domSelection.rangeCount > 0) {
domSelection.removeAllRanges()
}
if (selection) {
Transforms.deselect(editor)
}
},
/**
* Check if a DOM node is within the editor.
*/
hasDOMNode(
editor: VueEditor,
target: DOMNode,
options: { editable?: boolean } = {}
): boolean {
const { editable = false } = options
const editorEl = VueEditor.toDOMNode(editor, editor)
let targetEl
// COMPAT: In Firefox, reading `target.nodeType` will throw an error if
// target is originating from an internal "restricted" element (e.g. a
// stepper arrow on a number input). (2018/05/04)
// https://github.com/ianstormtaylor/slate/issues/1819
try {
targetEl = (isDOMElement(target)
? target
: target.parentElement) as HTMLElement
} catch (err) {
if (
!err.message.includes('Permission denied to access property "nodeType"')
) {
throw err
}
}
if (!targetEl) {
return false
}
return (
targetEl.closest(`[data-slate-editor]`) === editorEl &&
(!editable ||
targetEl.isContentEditable ||
!!targetEl.getAttribute('data-slate-zero-width'))
)
},
/**
* Insert data from a `DataTransfer` into the editor.
*/
insertData(editor: VueEditor, data: DataTransfer): void {
editor.insertData(data)
},
/**
* Sets data from the currently selected fragment on a `DataTransfer`.
*/
setFragmentData(editor: VueEditor, data: DataTransfer): void {
editor.setFragmentData(data)
},
/**
* Find the native DOM element from a Slate node.
*/
toDOMNode(editor: VueEditor, node: Node): HTMLElement {
const domNode = Editor.isEditor(node)
? EDITOR_TO_ELEMENT.get(editor)
: KEY_TO_ELEMENT.get(VueEditor.findKey(editor, node))
if (!domNode) {
throw new Error(
`Cannot resolve a DOM node from Slate node: ${JSON.stringify(node)}`
)
}
return domNode
},
/**
* Find a native DOM selection point from a Slate point.
*/
toDOMPoint(editor: VueEditor, point: Point): DOMPoint {
const [node] = Editor.node(editor, point.path)
const el = VueEditor.toDOMNode(editor, node)
let domPoint: DOMPoint | undefined
// If we're inside a void node, force the offset to 0, otherwise the zero
// width spacing character will result in an incorrect offset of 1
if (Editor.void(editor, { at: point })) {
point = { path: point.path, offset: 0 }
}
// For each leaf, we need to isolate its content, which means filtering
// to its direct text and zero-width spans. (We have to filter out any
// other siblings that may have been rendered alongside them.)
const selector = `[data-slate-string], [data-slate-zero-width]`
const texts = Array.from(el.querySelectorAll(selector))
let start = 0
for (const text of texts) {
const domNode = text.childNodes[0] as HTMLElement
if (domNode == null || domNode.textContent == null) {
continue
}
const { length } = domNode.textContent
const attr = text.getAttribute('data-slate-length')
const trueLength = attr == null ? length : parseInt(attr, 10)
const end = start + trueLength
if (point.offset <= end) {
const offset = Math.min(length, Math.max(0, point.offset - start))
domPoint = [domNode, offset]
break
}
start = end
}
if (!domPoint) {
throw new Error(
`Cannot resolve a DOM point from Slate point: ${JSON.stringify(point)}`
)
}
return domPoint
},
/**
* Find a native DOM range from a Slate `range`.
*
* Notice: the returned range will always be ordinal regardless of the direction of Slate `range` due to DOM API limit.
*
* there is no way to create a reverse DOM Range using Range.setStart/setEnd
* according to https://dom.spec.whatwg.org/#concept-range-bp-set.
*/
toDOMRange(editor: VueEditor, range: Range): DOMRange {
const { anchor, focus } = range
const isBackward = Range.isBackward(range)
const domAnchor = VueEditor.toDOMPoint(editor, anchor)
const domFocus = Range.isCollapsed(range)
? domAnchor
: VueEditor.toDOMPoint(editor, focus)
const domRange = window.document.createRange()
const [startNode, startOffset] = isBackward ? domFocus : domAnchor
const [endNode, endOffset] = isBackward ? domAnchor : domFocus
// A slate Point at zero-width Leaf always has an offset of 0 but a native DOM selection at
// zero-width node has an offset of 1 so we have to check if we are in a zero-width node and
// adjust the offset accordingly.
const startEl = (isDOMElement(startNode)
? startNode
: startNode.parentElement) as HTMLElement
const isStartAtZeroWidth = !!startEl.getAttribute('data-slate-zero-width')
const endEl = (isDOMElement(endNode)
? endNode
: endNode.parentElement) as HTMLElement
const isEndAtZeroWidth = !!endEl.getAttribute('data-slate-zero-width')
domRange.setStart(startNode, isStartAtZeroWidth ? 1 : startOffset)
domRange.setEnd(endNode, isEndAtZeroWidth ? 1 : endOffset)
return domRange
},
/**
* Find a Slate node from a native DOM `element`.
*/
toSlateNode(editor: VueEditor, domNode: DOMNode): Node {
let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement
if (domEl && !domEl.hasAttribute('data-slate-node')) {
domEl = domEl.closest(`[data-slate-node]`)
}
const node = domEl ? ELEMENT_TO_NODE.get(domEl as HTMLElement) : null
if (!node) {
throw new Error(`Cannot resolve a Slate node from DOM node: ${domEl}`)
}
return node
},
/**
* Get the target range from a DOM `event`.
*/
findEventRange(editor: VueEditor, event: any): Range {
if ('nativeEvent' in event) {
event = event.nativeEvent
}
const { clientX: x, clientY: y, target } = event
if (x == null || y == null) {
throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`)
}
const node = VueEditor.toSlateNode(editor, event.target)
const path = VueEditor.findPath(editor, node)
// If the drop target is inside a void node, move it into either the
// next or previous node, depending on which side the `x` and `y`
// coordinates are closest to.
if (Editor.isVoid(editor, node)) {
const rect = target.getBoundingClientRect()
const isPrev = editor.isInline(node)
? x - rect.left < rect.left + rect.width - x
: y - rect.top < rect.top + rect.height - y
const edge = Editor.point(editor, path, {
edge: isPrev ? 'start' : 'end',
})
const point = isPrev
? Editor.before(editor, edge)
: Editor.after(editor, edge)
if (point) {
const range = Editor.range(editor, point)
return range
}
}
// Else resolve a range from the caret position where the drop occured.
let domRange
const { document } = window
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
if (document.caretRangeFromPoint) {
domRange = document.caretRangeFromPoint(x, y)
} else {
const position = document.caretPositionFromPoint(x, y)
if (position) {
domRange = document.createRange()
domRange.setStart(position.offsetNode, position.offset)
domRange.setEnd(position.offsetNode, position.offset)
}
}
if (!domRange) {
throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`)
}
// Resolve a Slate range from the DOM range.
const range = VueEditor.toSlateRange(editor, domRange)
return range
},
/**
* Find a Slate point from a DOM selection's `domNode` and `domOffset`.
*/
toSlatePoint(editor: VueEditor, domPoint: DOMPoint): Point {
const [nearestNode, nearestOffset] = normalizeDOMPoint(domPoint)
const parentNode = nearestNode.parentNode as DOMElement
let textNode: DOMElement | null = null
let offset = 0
if (parentNode) {
const voidNode = parentNode.closest('[data-slate-void="true"]')
let leafNode = parentNode.closest('[data-slate-leaf]')
let domNode: DOMElement | null = null
// Calculate how far into the text node the `nearestNode` is, so that we
// can determine what the offset relative to the text node is.
if (leafNode) {
textNode = leafNode.closest('[data-slate-node="text"]')!
const range = window.document.createRange()
range.setStart(textNode, 0)
range.setEnd(nearestNode, nearestOffset)
const contents = range.cloneContents()
const removals = [
...contents.querySelectorAll('[data-slate-zero-width]'),
...contents.querySelectorAll('[contenteditable=false]'),
]
removals.forEach(el => {
el!.parentNode!.removeChild(el)
})
// COMPAT: Edge has a bug where Range.prototype.toString() will
// convert \n into \r\n. The bug causes a loop when slate-react
// attempts to reposition its cursor to match the native position. Use
// textContent.length instead.
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/
offset = contents.textContent!.length
domNode = textNode
} else if (voidNode) {
// For void nodes, the element with the offset key will be a cousin, not an
// ancestor, so find it by going down from the nearest void parent.
leafNode = voidNode.querySelector('[data-slate-leaf]')!
textNode = leafNode.closest('[data-slate-node="text"]')!
domNode = leafNode
offset = domNode.textContent!.length
}
// COMPAT: If the parent node is a Slate zero-width space, editor is
// because the text node should have no characters. However, during IME
// composition the ASCII characters will be prepended to the zero-width
// space, so subtract 1 from the offset to account for the zero-width
// space character.
if (
domNode &&
offset === domNode.textContent!.length &&
parentNode.hasAttribute('data-slate-zero-width')
) {
offset--
}
}
if (!textNode) {
throw new Error(
`Cannot resolve a Slate point from DOM point: ${domPoint}`
)
}
// COMPAT: If someone is clicking from one Slate editor into another,
// the select event fires twice, once for the old editor's `element`
// first, and then afterwards for the correct `element`. (2017/03/03)
const slateNode = VueEditor.toSlateNode(editor, textNode!)
const path = VueEditor.findPath(editor, slateNode)
return { path, offset }
},
/**
* Find a Slate range from a DOM range or selection.
*/
toSlateRange(
editor: VueEditor,
domRange: DOMRange | DOMStaticRange | DOMSelection
): Range {
const el =
domRange instanceof Selection
? domRange.anchorNode
: domRange.startContainer
let anchorNode
let anchorOffset
let focusNode
let focusOffset
let isCollapsed
if (el) {
if (domRange instanceof Selection) {
anchorNode = domRange.anchorNode
anchorOffset = domRange.anchorOffset
focusNode = domRange.focusNode
focusOffset = domRange.focusOffset
isCollapsed = domRange.isCollapsed
} else {
anchorNode = domRange.startContainer
anchorOffset = domRange.startOffset
focusNode = domRange.endContainer
focusOffset = domRange.endOffset
isCollapsed = domRange.collapsed
}
}
if (
anchorNode == null ||
focusNode == null ||
anchorOffset == null ||
focusOffset == null
) {
throw new Error(
`Cannot resolve a Slate range from DOM range: ${domRange}`
)
}
const anchor = VueEditor.toSlatePoint(editor, [anchorNode, anchorOffset])
const focus = isCollapsed
? anchor
: VueEditor.toSlatePoint(editor, [focusNode, focusOffset])
return { anchor, focus }
},
}
Example #8
Source File: withShortcuts.ts From ui-schema with MIT License | 4 votes |
withShortcuts: (options: CustomOptions) => SlateHocType<ReactEditor> = (options) => (editor) => {
if(options.onlyInline){
return editor
}
const {deleteBackward, insertText} = editor
editor.insertText = (text: string) => {
const {selection} = editor
if (text === ' ' && selection && Range.isCollapsed(selection)) {
const {anchor} = selection
const block = Editor.above(editor, {
match: n => Editor.isBlock(editor, n),
})
const path = block ? block[1] : []
const start = Editor.start(editor, path)
const range = {anchor, focus: start}
let beforeText = Editor.string(editor, range)
const dotPos = beforeText.indexOf('.')
if (dotPos !== -1 && !isNaN(Number(beforeText.slice(0, dotPos)))) {
beforeText = '1'
}
const type = SHORTCUTS[beforeText]
if (
type &&
editorIsEnabled(options.enableOnly, type) &&
// only apply shortcuts when not inside a li, as this may produce invalid data
// @ts-ignore
!(block && block[0].type === pluginOptions.li.type)
) {
Transforms.select(editor, range)
Transforms.delete(editor)
const newProperties: Partial<SlateElement> = {
// @ts-ignore
type: type === pluginOptions.ol.type ? pluginOptions.li.type : type,
}
Transforms.setNodes(editor, newProperties, {
match: n => Editor.isBlock(editor, n),
})
if (type === pluginOptions.ol.type) {
const list: BulletedListElement = {
type: pluginOptions.ol.type,
children: [],
}
// todo: here seems to be the `li` error, currently missing `ul` around `li`
Transforms.wrapNodes(editor, list, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
// @ts-ignore
n.type === pluginOptions.li.type,
})
} else if (type === pluginOptions.li.type) {
const list: BulletedListElement = {
type: pluginOptions.ul.type,
children: [],
}
// todo: here seems to be the `li` error, currently missing `ul` around `li`
Transforms.wrapNodes(editor, list, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
// @ts-ignore
n.type === pluginOptions.li.type,
})
}
return
}
}
insertText(text)
}
editor.deleteBackward = (...args) => {
const {selection} = editor
if (selection && Range.isCollapsed(selection)) {
const match = Editor.above(editor, {
match: n => Editor.isBlock(editor, n),
})
if (match) {
const [block, path] = match
const start = Editor.start(editor, path)
if (
!Editor.isEditor(block) &&
SlateElement.isElement(block) &&
// @ts-ignore
block.type !== pluginOptions.p.type &&
Point.equals(selection.anchor, start)
) {
const newProperties: Partial<SlateElement> = {
// @ts-ignore
type: pluginOptions.p.type,
}
Transforms.setNodes(editor, newProperties)
// @ts-ignore
if (block.type === pluginOptions.li.type) {
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
(
// @ts-ignore
n.type === pluginOptions.ul.type ||
// @ts-ignore
n.type === pluginOptions.ol.type
),
split: true,
})
}
return
}
}
deleteBackward(...args)
}
}
return editor
}
Example #9
Source File: angular-editor.ts From slate-angular with MIT License | 4 votes |
AngularEditor = {
/**
* Return the host window of the current editor.
*/
getWindow(editor: AngularEditor): Window {
const window = EDITOR_TO_WINDOW.get(editor);
if (!window) {
throw new Error('Unable to find a host window element for this editor');
}
return window;
},
/**
* Find a key for a Slate node.
*/
findKey(editor: AngularEditor, node: Node): Key {
let key = NODE_TO_KEY.get(node);
if (!key) {
key = new Key();
NODE_TO_KEY.set(node, key);
}
return key;
},
/**
* handle editor error.
*/
onError(errorData: SlateError) {
if (errorData.nativeError) {
throw errorData.nativeError;
}
},
/**
* Find the path of Slate node.
*/
findPath(editor: AngularEditor, node: Node): Path {
const path: Path = [];
let child = node;
while (true) {
const parent = NODE_TO_PARENT.get(child);
if (parent == null) {
if (Editor.isEditor(child)) {
return path;
} else {
break;
}
}
const i = NODE_TO_INDEX.get(child);
if (i == null) {
break;
}
path.unshift(i);
child = parent;
}
throw new Error(`Unable to find the path for Slate node: ${JSON.stringify(node)}`);
},
/**
* Find the DOM node that implements DocumentOrShadowRoot for the editor.
*/
findDocumentOrShadowRoot(editor: AngularEditor): Document | ShadowRoot {
const el = AngularEditor.toDOMNode(editor, editor)
const root = el.getRootNode()
if (
(root instanceof Document || root instanceof ShadowRoot) &&
(root as Document).getSelection != null
) {
return root
}
return el.ownerDocument
},
/**
* Check if the editor is focused.
*/
isFocused(editor: AngularEditor): boolean {
return !!IS_FOCUSED.get(editor);
},
/**
* Check if the editor is in read-only mode.
*/
isReadonly(editor: AngularEditor): boolean {
return !!IS_READONLY.get(editor);
},
/**
* Check if the editor is hanging right.
*/
isBlockHangingRight(editor: AngularEditor): boolean {
const { selection } = editor;
if (!selection) {
return false;
}
if (Range.isCollapsed(selection)) {
return false;
}
const [start, end] = Range.edges(selection);
const endBlock = Editor.above(editor, { at: end, match: (node) => Editor.isBlock(editor, node) });
return Editor.isStart(editor, end, endBlock[1]);
},
/**
* Blur the editor.
*/
blur(editor: AngularEditor): void {
const el = AngularEditor.toDOMNode(editor, editor);
const root = AngularEditor.findDocumentOrShadowRoot(editor);
IS_FOCUSED.set(editor, false);
if (root.activeElement === el) {
el.blur();
}
},
/**
* Focus the editor.
*/
focus(editor: AngularEditor): void {
const el = AngularEditor.toDOMNode(editor, editor);
IS_FOCUSED.set(editor, true);
const window = AngularEditor.getWindow(editor);
if (window.document.activeElement !== el) {
el.focus({ preventScroll: true });
}
},
/**
* Deselect the editor.
*/
deselect(editor: AngularEditor): void {
const { selection } = editor;
const root = AngularEditor.findDocumentOrShadowRoot(editor);
const domSelection = (root as Document).getSelection();
if (domSelection && domSelection.rangeCount > 0) {
domSelection.removeAllRanges();
}
if (selection) {
Transforms.deselect(editor);
}
},
/**
* Check if a DOM node is within the editor.
*/
hasDOMNode(editor: AngularEditor, target: DOMNode, options: { editable?: boolean } = {}): boolean {
const { editable = false } = options;
const editorEl = AngularEditor.toDOMNode(editor, editor);
let targetEl;
// COMPAT: In Firefox, reading `target.nodeType` will throw an error if
// target is originating from an internal "restricted" element (e.g. a
// stepper arrow on a number input). (2018/05/04)
// https://github.com/ianstormtaylor/slate/issues/1819
try {
targetEl = (isDOMElement(target) ? target : target.parentElement) as HTMLElement;
} catch (err) {
if (!err.message.includes('Permission denied to access property "nodeType"')) {
throw err;
}
}
if (!targetEl) {
return false;
}
return targetEl.closest(`[data-slate-editor]`) === editorEl &&
(!editable || targetEl.isContentEditable ||
!!targetEl.getAttribute('data-slate-zero-width'));
},
/**
* Insert data from a `DataTransfer` into the editor.
*/
insertData(editor: AngularEditor, data: DataTransfer): void {
editor.insertData(data);
},
/**
* Insert fragment data from a `DataTransfer` into the editor.
*/
insertFragmentData(editor: AngularEditor, data: DataTransfer): boolean {
return editor.insertFragmentData(data)
},
/**
* Insert text data from a `DataTransfer` into the editor.
*/
insertTextData(editor: AngularEditor, data: DataTransfer): boolean {
return editor.insertTextData(data)
},
/**
* onKeydown hook.
*/
onKeydown(editor: AngularEditor, data: KeyboardEvent): void {
editor.onKeydown(data);
},
/**
* onClick hook.
*/
onClick(editor: AngularEditor, data: MouseEvent): void {
editor.onClick(data);
},
/**
* Sets data from the currently selected fragment on a `DataTransfer`.
*/
setFragmentData(editor: AngularEditor, data: DataTransfer, originEvent?: 'drag' | 'copy' | 'cut'): void {
editor.setFragmentData(data, originEvent);
},
deleteCutData(editor: AngularEditor): void {
editor.deleteCutData();
},
/**
* Find the native DOM element from a Slate node.
*/
toDOMNode(editor: AngularEditor, node: Node): HTMLElement {
const domNode = Editor.isEditor(node)
? EDITOR_TO_ELEMENT.get(editor)
: NODE_TO_ELEMENT.get(node);
if (!domNode) {
throw new Error(`Cannot resolve a DOM node from Slate node: ${JSON.stringify(node)}`);
}
return domNode;
},
/**
* Find a native DOM selection point from a Slate point.
*/
toDOMPoint(editor: AngularEditor, point: Point): DOMPoint {
const [node] = Editor.node(editor, point.path);
const el = AngularEditor.toDOMNode(editor, node);
let domPoint: DOMPoint | undefined;
// block card
const cardTargetAttr = getCardTargetAttribute(el);
if (cardTargetAttr) {
if (point.offset === FAKE_LEFT_BLOCK_CARD_OFFSET) {
const cursorNode = AngularEditor.getCardCursorNode(editor, node, { direction: 'left' });
return [cursorNode, 1];
} else {
const cursorNode = AngularEditor.getCardCursorNode(editor, node, { direction: 'right' });
return [cursorNode, 1];
}
}
// If we're inside a void node, force the offset to 0, otherwise the zero
// width spacing character will result in an incorrect offset of 1
if (Editor.void(editor, { at: point })) {
point = { path: point.path, offset: 0 };
}
// For each leaf, we need to isolate its content, which means filtering
// to its direct text and zero-width spans. (We have to filter out any
// other siblings that may have been rendered alongside them.)
const selector = `[data-slate-string], [data-slate-zero-width]`;
const texts = Array.from(el.querySelectorAll(selector));
let start = 0;
for (const text of texts) {
const domNode = text.childNodes[0] as HTMLElement;
if (domNode == null || domNode.textContent == null) {
continue;
}
const { length } = domNode.textContent;
const attr = text.getAttribute('data-slate-length');
const trueLength = attr == null ? length : parseInt(attr, 10);
const end = start + trueLength;
if (point.offset <= end) {
const offset = Math.min(length, Math.max(0, point.offset - start));
domPoint = [domNode, offset];
// fixed cursor position after zero width char
if (offset === 0 && length === 1 && domNode.textContent === '\uFEFF') {
domPoint = [domNode, offset + 1];
}
break;
}
start = end;
}
if (!domPoint) {
throw new Error(`Cannot resolve a DOM point from Slate point: ${JSON.stringify(point)}`);
}
return domPoint;
},
/**
* Find a native DOM range from a Slate `range`.
*/
toDOMRange(editor: AngularEditor, range: Range): DOMRange {
const { anchor, focus } = range;
const isBackward = Range.isBackward(range);
const domAnchor = AngularEditor.toDOMPoint(editor, anchor);
const domFocus = Range.isCollapsed(range) ? domAnchor : AngularEditor.toDOMPoint(editor, focus);
const window = AngularEditor.getWindow(editor);
const domRange = window.document.createRange();
const [startNode, startOffset] = isBackward ? domFocus : domAnchor;
const [endNode, endOffset] = isBackward ? domAnchor : domFocus;
// A slate Point at zero-width Leaf always has an offset of 0 but a native DOM selection at
// zero-width node has an offset of 1 so we have to check if we are in a zero-width node and
// adjust the offset accordingly.
const startEl = (isDOMElement(startNode)
? startNode
: startNode.parentElement) as HTMLElement;
const isStartAtZeroWidth = !!startEl.getAttribute('data-slate-zero-width');
const endEl = (isDOMElement(endNode)
? endNode
: endNode.parentElement) as HTMLElement;
const isEndAtZeroWidth = !!endEl.getAttribute('data-slate-zero-width');
domRange.setStart(startNode, isStartAtZeroWidth ? 1 : startOffset);
domRange.setEnd(endNode, isEndAtZeroWidth ? 1 : endOffset);
return domRange;
},
/**
* Find a Slate node from a native DOM `element`.
*/
toSlateNode(editor: AngularEditor, domNode: DOMNode): Node {
let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement;
if (domEl && !domEl.hasAttribute('data-slate-node')) {
domEl = domEl.closest(`[data-slate-node]`);
}
const node = domEl ? ELEMENT_TO_NODE.get(domEl as HTMLElement) : null;
if (!node) {
throw new Error(`Cannot resolve a Slate node from DOM node: ${domEl}`);
}
return node;
},
/**
* Get the target range from a DOM `event`.
*/
findEventRange(editor: AngularEditor, event: any): Range {
if ('nativeEvent' in event) {
event = event.nativeEvent;
}
const { clientX: x, clientY: y, target } = event;
if (x == null || y == null) {
throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
}
const node = AngularEditor.toSlateNode(editor, event.target);
const path = AngularEditor.findPath(editor, node);
// If the drop target is inside a void node, move it into either the
// next or previous node, depending on which side the `x` and `y`
// coordinates are closest to.
if (Editor.isVoid(editor, node)) {
const rect = target.getBoundingClientRect();
const isPrev = editor.isInline(node)
? x - rect.left < rect.left + rect.width - x
: y - rect.top < rect.top + rect.height - y;
const edge = Editor.point(editor, path, {
edge: isPrev ? 'start' : 'end'
});
const point = isPrev ? Editor.before(editor, edge) : Editor.after(editor, edge);
if (point) {
return Editor.range(editor, point);
}
}
// Else resolve a range from the caret position where the drop occured.
let domRange: DOMRange;
const window = AngularEditor.getWindow(editor);
const { document } = window;
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
if (document.caretRangeFromPoint) {
domRange = document.caretRangeFromPoint(x, y);
} else {
const position = (document as SafeAny).caretPositionFromPoint(x, y);
if (position) {
domRange = document.createRange();
domRange.setStart(position.offsetNode, position.offset);
domRange.setEnd(position.offsetNode, position.offset);
}
}
if (!domRange) {
throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
}
// Resolve a Slate range from the DOM range.
const range = AngularEditor.toSlateRange(editor, domRange);
return range;
},
/**
* Find a Slate point from a DOM selection's `domNode` and `domOffset`.
*/
toSlatePoint(editor: AngularEditor, domPoint: DOMPoint): Point {
const [domNode] = domPoint;
const [nearestNode, nearestOffset] = normalizeDOMPoint(domPoint);
let parentNode = nearestNode.parentNode as DOMElement;
let textNode: DOMElement | null = null;
let offset = 0;
// block card
const cardTargetAttr = getCardTargetAttribute(domNode);
if (cardTargetAttr) {
const domSelection = window.getSelection();
const isBackward = editor.selection && Range.isBackward(editor.selection);
const blockCardEntry = AngularEditor.toSlateCardEntry(editor, domNode) || AngularEditor.toSlateCardEntry(editor, nearestNode);
const [, blockPath] = blockCardEntry;
if (domSelection.isCollapsed) {
if (isCardLeftByTargetAttr(cardTargetAttr)) {
return { path: blockPath, offset: -1 };
}
else {
return { path: blockPath, offset: -2 };
}
}
// forward
// and to the end of previous node
if (isCardLeftByTargetAttr(cardTargetAttr) && !isBackward) {
const endPath =
blockPath[blockPath.length - 1] <= 0
? blockPath
: Path.previous(blockPath);
return Editor.end(editor, endPath);
}
// to the of current node
if (
(isCardCenterByTargetAttr(cardTargetAttr) ||
isCardRightByTargetAttr(cardTargetAttr)) &&
!isBackward
) {
return Editor.end(editor, blockPath);
}
// backward
// and to the start of next node
if (isCardRightByTargetAttr(cardTargetAttr) && isBackward) {
return Editor.start(editor, Path.next(blockPath));
}
// and to the start of current node
if (
(isCardCenterByTargetAttr(cardTargetAttr) ||
isCardLeftByTargetAttr(cardTargetAttr)) &&
isBackward
) {
return Editor.start(editor, blockPath);
}
}
if (parentNode) {
const voidNode = parentNode.closest('[data-slate-void="true"]');
let leafNode = parentNode.closest('[data-slate-leaf]');
let domNode: DOMElement | null = null;
// Calculate how far into the text node the `nearestNode` is, so that we
// can determine what the offset relative to the text node is.
if (leafNode) {
textNode = leafNode.closest('[data-slate-node="text"]')!;
const window = AngularEditor.getWindow(editor);
const range = window.document.createRange();
range.setStart(textNode, 0);
range.setEnd(nearestNode, nearestOffset);
const contents = range.cloneContents();
const removals = [
...Array.prototype.slice.call(
contents.querySelectorAll('[data-slate-zero-width]')
),
...Array.prototype.slice.call(
contents.querySelectorAll('[contenteditable=false]')
),
];
removals.forEach(el => {
el!.parentNode!.removeChild(el);
});
// COMPAT: Edge has a bug where Range.prototype.toString() will
// convert \n into \r\n. The bug causes a loop when slate-react
// attempts to reposition its cursor to match the native position. Use
// textContent.length instead.
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/
offset = contents.textContent!.length;
domNode = textNode;
} else if (voidNode) {
// For void nodes, the element with the offset key will be a cousin, not an
// ancestor, so find it by going down from the nearest void parent.
leafNode = voidNode.querySelector('[data-slate-leaf]')!;
parentNode = voidNode.querySelector('[data-slate-length="0"]');
textNode = leafNode.closest('[data-slate-node="text"]')!;
domNode = leafNode;
offset = domNode.textContent!.length;
}
// COMPAT: If the parent node is a Slate zero-width space, editor is
// because the text node should have no characters. However, during IME
// composition the ASCII characters will be prepended to the zero-width
// space, so subtract 1 from the offset to account for the zero-width
// space character.
if (domNode &&
offset === domNode.textContent!.length &&
(parentNode && parentNode.hasAttribute('data-slate-zero-width'))
) {
offset--;
}
}
if (!textNode) {
throw new Error(`Cannot resolve a Slate point from DOM point: ${domPoint}`);
}
// COMPAT: If someone is clicking from one Slate editor into another,
// the select event fires twice, once for the old editor's `element`
// first, and then afterwards for the correct `element`. (2017/03/03)
const slateNode = AngularEditor.toSlateNode(editor, textNode!);
const path = AngularEditor.findPath(editor, slateNode);
return { path, offset };
},
/**
* Find a Slate range from a DOM range or selection.
*/
toSlateRange(editor: AngularEditor, domRange: DOMRange | DOMStaticRange | DOMSelection): Range {
const el = isDOMSelection(domRange) ? domRange.anchorNode : domRange.startContainer;
let anchorNode;
let anchorOffset;
let focusNode;
let focusOffset;
let isCollapsed;
if (el) {
if (isDOMSelection(domRange)) {
anchorNode = domRange.anchorNode;
anchorOffset = domRange.anchorOffset;
focusNode = domRange.focusNode;
focusOffset = domRange.focusOffset;
// COMPAT: There's a bug in chrome that always returns `true` for
// `isCollapsed` for a Selection that comes from a ShadowRoot.
// (2020/08/08)
// https://bugs.chromium.org/p/chromium/issues/detail?id=447523
if (IS_CHROME && hasShadowRoot()) {
isCollapsed =
domRange.anchorNode === domRange.focusNode &&
domRange.anchorOffset === domRange.focusOffset;
} else {
isCollapsed = domRange.isCollapsed;
}
} else {
anchorNode = domRange.startContainer;
anchorOffset = domRange.startOffset;
focusNode = domRange.endContainer;
focusOffset = domRange.endOffset;
isCollapsed = domRange.collapsed;
}
}
if (anchorNode == null || focusNode == null || anchorOffset == null || focusOffset == null) {
throw new Error(`Cannot resolve a Slate range from DOM range: ${domRange}`);
}
const anchor = AngularEditor.toSlatePoint(editor, [anchorNode, anchorOffset]);
const focus = isCollapsed ? anchor : AngularEditor.toSlatePoint(editor, [focusNode, focusOffset]);
return { anchor, focus };
},
isLeafBlock(editor: AngularEditor, node: Node): boolean {
return Element.isElement(node) && !editor.isInline(node) && Editor.hasInlines(editor, node);
},
isBlockCardLeftCursor(editor: AngularEditor) {
return editor.selection.anchor.offset === FAKE_LEFT_BLOCK_CARD_OFFSET && editor.selection.focus.offset === FAKE_LEFT_BLOCK_CARD_OFFSET;
},
isBlockCardRightCursor(editor: AngularEditor) {
return editor.selection.anchor.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET && editor.selection.focus.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET;
},
getCardCursorNode(editor: AngularEditor, blockCardNode: Node, options: {
direction: 'left' | 'right' | 'center'
}) {
const blockCardElement = AngularEditor.toDOMNode(editor, blockCardNode);
const cardCenter = blockCardElement.parentElement;
return options.direction === 'left'
? cardCenter.previousElementSibling
: cardCenter.nextElementSibling;
},
toSlateCardEntry(editor: AngularEditor, node: DOMNode): NodeEntry {
const element = node.parentElement
.closest('.slate-block-card')?.querySelector('[card-target="card-center"]')
.firstElementChild;
const slateNode = AngularEditor.toSlateNode(editor, element);
const path = AngularEditor.findPath(editor, slateNode);
return [slateNode, path];
},
/**
* move native selection to card-left or card-right
* @param editor
* @param blockCardNode
* @param options
*/
moveBlockCard(editor: AngularEditor, blockCardNode: Node, options: {
direction: 'left' | 'right'
}) {
const cursorNode = AngularEditor.getCardCursorNode(editor, blockCardNode, options);
const window = AngularEditor.getWindow(editor);
const domSelection = window.getSelection();
domSelection.setBaseAndExtent(cursorNode, 1, cursorNode, 1);
},
/**
* move slate selection to card-left or card-right
* @param editor
* @param path
* @param options
*/
moveBlockCardCursor(editor: AngularEditor, path: Path, options: {
direction: 'left' | 'right'
}) {
const cursor = { path, offset: options.direction === 'left' ? FAKE_LEFT_BLOCK_CARD_OFFSET : FAKE_RIGHT_BLOCK_CARD_OFFSET };
Transforms.select(editor, { anchor: cursor, focus: cursor });
},
hasRange(editor: AngularEditor, range: Range): boolean {
const { anchor, focus } = range;
return (
Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path)
);
},
}