feat(): 优化了一点性能问题

This commit is contained in:
yanmao 2021-11-20 17:17:50 +08:00
parent 759ba6de57
commit c1cf65a254
51 changed files with 672 additions and 649 deletions

View File

@ -144,24 +144,6 @@ Is it an engine
Accepted object: `EditorInterface`
### `getWindow`
Get the window object from the node
If window is undefined, it will try to get the window object from global['__amWindow']
```ts
(node?: Node): Window & typeof globalThis
```
### `getDocument`
Get the document object from the node
```ts
getDocument(node?: Node): Document
```
### `combinText`
Remove empty text nodes and connect adjacent text nodes

View File

@ -144,24 +144,6 @@
接受对象:`EditorInterface`
### `getWindow`
从节点中获取 window 对象
如果 window 是 undefined 会尝试从 global['__amWindow'] 中获取 window 对象
```ts
(node?: Node): Window & typeof globalThis
```
### `getDocument`
从节点中获取 document 对象
```ts
getDocument(node?: Node): Document
```
### `combinText`
移除空的文本节点,并连接相邻的文本节点

View File

@ -5,6 +5,7 @@
span[data-comment-preview], span[data-comment-id] {
display: inline-block;
text-indent: 0;
}
[data-comment-preview]::before, [data-comment-id]::before{

View File

@ -117,12 +117,12 @@ const EditorComponent: React.FC<EditorProps> = ({
// 获取编辑器的值
console.log(`value ${trigger} update:`, value);
// 获取当前所有at插件中的名单
console.log(
'mention:',
engine.current?.command.executeMethod('mention', 'getList'),
);
// console.log(
// 'mention:',
// engine.current?.command.executeMethod('mention', 'getList'),
// );
// 获取编辑器的html
console.log('html:', engine.current?.getHtml());
//console.log('html:', engine.current?.getHtml());
},
[loading, autoSave, props.onChange],
),
@ -132,13 +132,11 @@ const EditorComponent: React.FC<EditorProps> = ({
const userSave = useCallback(() => {
if (!engine.current) return;
//获取异步的值,有些组件可能还在处理中,比如正在上传
console.time('value-async');
engine.current
.getValueAsync(false, (pluginName, card) => {
console.log(`${pluginName} 正在等待...`, card?.getValue());
})
.then((value) => {
console.timeEnd('value-async');
setValue(value);
save();
})

View File

@ -16,7 +16,7 @@ import {
PluginEntry,
} from '../types';
import { BlockInterface, BlockModelInterface } from '../types/block';
import { getDocument, getWindow, isEngine } from '../utils';
import { getDocument, isEngine } from '../utils';
import { Backspace, Enter } from './typing';
import { $ } from '../node';
import { isBlockPlugin } from '../plugin';
@ -865,13 +865,13 @@ class Block implements BlockModelInterface {
let startNode = sc;
let endNode = ec;
if (sc.nodeType === getWindow().Node.ELEMENT_NODE) {
if (sc.nodeType === Node.ELEMENT_NODE) {
if (sc.childNodes[so]) {
startNode = sc.childNodes[so] || sc;
}
}
if (ec.nodeType === getWindow().Node.ELEMENT_NODE) {
if (ec.nodeType === Node.ELEMENT_NODE) {
if (eo > 0 && ec.childNodes[eo - 1]) {
endNode = ec.childNodes[eo - 1] || sc;
}
@ -1232,13 +1232,15 @@ class Block implements BlockModelInterface {
* @param block
*/
brToBlock(block: NodeInterface) {
const first = block.first();
// 没有子节点
if (!block.first()) {
if (!first) {
return;
}
const children = block.children();
// 只有一个节点
if (block.children().length === 1) {
const node = block.first();
if (children.length === 1) {
const node = first;
//\n换成 br
if (node && node.isText() && /^\n+$/g.test(node.text())) {
this.editor.node.replace(node, $('<br />'));
@ -1248,16 +1250,18 @@ class Block implements BlockModelInterface {
if ('li' === block.name) return;
// 只有一个节点(有光标标记节点)
if (
(block.children().length === 2 &&
block.first()?.attributes(DATA_ELEMENT) === CURSOR) ||
(children.length === 2 &&
first?.attributes(DATA_ELEMENT) === CURSOR) ||
block.last()?.attributes(DATA_ELEMENT) === CURSOR
) {
return;
}
if (block.find('br').length === 0) return;
let container;
let prevContainer;
let node = block.first();
let node: NodeInterface | null = first;
while (node) {
const next = node.next();
if (!container || node.name === 'br') {
@ -1337,7 +1341,7 @@ class Block implements BlockModelInterface {
cloneRange.enlargeFromTextNode();
if (
this.isLastOffset(range, 'end') ||
(cloneRange.endNode.type === getWindow().Node.ELEMENT_NODE &&
(cloneRange.endNode.type === Node.ELEMENT_NODE &&
block.children().length > 0 &&
cloneRange.endContainer.childNodes[cloneRange.endOffset] ===
block.last()?.get() &&

View File

@ -1,5 +1,4 @@
import { EngineInterface, RangeInterface } from '../../types';
import { getWindow } from '../../utils';
class Backspace {
private engine: EngineInterface;
@ -43,16 +42,14 @@ class Backspace {
.shrinkToElementNode()
.shrinkToTextNode();
if (
cloneRange.startContainer.nodeType ===
getWindow().Node.TEXT_NODE &&
cloneRange.startContainer.nodeType === Node.TEXT_NODE &&
(function (range: RangeInterface) {
const { commonAncestorContainer } = range;
if (
range.collapsed &&
1 === range.startOffset &&
range.startContainer === commonAncestorContainer &&
commonAncestorContainer.nodeType ===
getWindow().Node.TEXT_NODE
commonAncestorContainer.nodeType === Node.TEXT_NODE
) {
range = range.cloneRange();
if (

View File

@ -176,14 +176,11 @@ abstract class CardEntry<T extends CardValue = {}> implements CardInterface {
const body = this.root.first() || $([]);
if (key === 'body' || body.length === 0) return body;
const children = body.children();
if (['center', 'left', 'right'].includes(key))
return (
children
.toArray()
.find(
(child) => child.attributes(CARD_ELEMENT_KEY) === key,
) || $([])
);
const index = ['center', 'left', 'right'].indexOf(key);
if (index > -1) {
const child = children.eq(index);
if (child?.attributes(CARD_ELEMENT_KEY) === key) return child;
}
const tag = this.type === CardType.BLOCK ? 'div' : 'span';
return this.find(`${tag}[${CARD_ELEMENT_KEY}=${key}]`);
}

View File

@ -9,7 +9,7 @@ import { RangeInterface, RangePath } from '../types/range';
import ChangeEvent from './event';
import Parser from '../parser';
import { ANCHOR_SELECTOR, CURSOR_SELECTOR, FOCUS_SELECTOR } from '../constants';
import { combinText, getWindow } from '../utils';
import { combinText } from '../utils';
import { TRIGGER_CARD_ID } from '../constants/card';
import { DATA_ID, EDITABLE_SELECTOR, UI_SELECTOR } from '../constants/root';
import { SelectionInterface } from '../types/selection';
@ -26,7 +26,7 @@ class ChangeModel implements ChangeInterface {
valueCached: string | null = null;
onChange: (value: string, trigger: 'remote' | 'local' | 'both') => void;
onRealtimeChange: (trigger: 'remote' | 'local') => void;
onSelect: () => void;
onSelect: (range?: RangeInterface) => void;
onSetValue: () => void;
rangePathBeforeCommand?: { start: RangePath; end: RangePath };
marks: Array<NodeInterface> = [];
@ -43,14 +43,23 @@ class ChangeModel implements ChangeInterface {
this.onChange = this.options.onChange || function () {};
this.onRealtimeChange = this.options.onRealtimeChange || function () {};
this.onSelect = this.options.onSelect || function () {};
this.onSetValue = this.options.onSetValue || function () {};
this.range = new ChangeRange(engine, {
onSelect: (range) => {
this.onSelect = (range?: RangeInterface) => {
if (selectTimeout) clearTimeout(selectTimeout);
selectTimeout = setTimeout(() => {
const { mark, block, inline } = engine;
range = range || this.range.get();
this.marks = mark.findMarks(range);
this.blocks = block.findBlocks(range);
this.inlines = inline.findInlines(range);
if (this.options.onSelect) this.options.onSelect();
}, 50);
};
this.onSetValue = this.options.onSetValue || function () {};
let selectTimeout: null | NodeJS.Timeout = null;
this.range = new ChangeRange(engine, {
onSelect: (range) => {
this.onSelect(range);
},
});
this.#nativeEvent = new NativeEvent(engine);
@ -271,8 +280,8 @@ class ChangeModel implements ChangeInterface {
const { container, node, schema } = this.engine;
const tags = schema.getAllowInTags();
return (
node.isEmptyWithTrim(container) &&
container.children().length === 1 &&
node.isEmptyWithTrim(container) &&
!container.allChildren().some((child) => tags.includes(child.name))
);
}
@ -392,7 +401,9 @@ class ChangeModel implements ChangeInterface {
.enlargeToElementNode(true)
.collapse(false);
const startNode =
cloneRange.startContainer.childNodes[range.startOffset - 1];
cloneRange.startContainer.childNodes[
range.startOffset === 0 ? 0 : range.startOffset - 1
];
const endNode = cloneRange.startContainer.childNodes[range.startOffset];
if (childNodes.length !== 0) {
@ -633,8 +644,7 @@ class ChangeModel implements ChangeInterface {
if (!isEmptyNode && startNode.length > 0 && startNode.inEditor()) {
if (
startNode[0].childNodes.length === 1 &&
startNode[0].firstChild?.nodeType ===
getWindow().Node.ELEMENT_NODE &&
startNode[0].firstChild?.nodeType === Node.ELEMENT_NODE &&
nodeApi.isCustomize(startNode) &&
startNode.first()?.isCard()
)

View File

@ -242,8 +242,72 @@ class NativeEvent {
}
}
handleSelectionChange() {
const { change, container, card } = this.engine;
if (change.isComposing()) return;
const { window } = container;
const selection = window?.getSelection();
if (selection && selection.anchorNode) {
const range = Range.from(this.engine, selection)!;
// 判断当前光标是否包含卡片或者在卡片内部
let containsCard =
range.containsCard() ||
(range.commonAncestorNode.closest(CARD_SELECTOR).length > 0 &&
range.startNode.closest(
`${CARD_LEFT_SELECTOR},${CARD_RIGHT_SELECTOR},${UI_SELECTOR}`,
).length === 0);
let isSingle = range.collapsed;
if (!isSingle) {
const { startNode, endNode, startOffset, endOffset } = range;
const startElement =
startNode.isElement() && !startNode.isCard()
? startNode.children().eq(startOffset)
: startNode;
const endElement =
endNode.isElement() && !endNode.isCard()
? endNode.children().eq(endOffset - 1)
: endNode;
if (
startElement &&
endElement &&
startElement.isCard() &&
startElement.equal(endElement)
) {
isSingle = true;
}
}
card.each((card) => {
const center = card.getCenter();
if (center && center.length > 0) {
let isSelect = selection.containsNode(center[0]);
if (!isSelect && containsCard && selection.focusNode) {
const focusCard = this.engine.card.find(
selection.focusNode,
);
if (focusCard) {
isSingle =
!selection.anchorNode ||
focusCard.root.contains(selection.anchorNode);
if (isSingle && card.root.equal(focusCard.root)) {
isSelect = true;
}
}
// 找到一次其它的就不需要再去比对了
if (isSelect && isSingle) containsCard = false;
}
const { autoSelected } = card.constructor as CardEntry;
card.select(
isSelect && (!isSingle || autoSelected !== false),
);
}
});
}
}
init() {
const { change, container, card, clipboard } = this.engine;
const { change, card, clipboard } = this.engine;
change.event.onInput((event: InputEvent) => {
const range = change.range.get();
@ -252,75 +316,13 @@ class NativeEvent {
change.onSelect();
change.change();
});
let selectionChangeTimeout: NodeJS.Timeout | undefined = undefined;
change.event.onDocument('selectionchange', () => {
if (change.isComposing()) return;
const { window } = container;
const selection = window?.getSelection();
if (selection && selection.anchorNode) {
const range = Range.from(this.engine, selection)!;
// 判断当前光标是否包含卡片或者在卡片内部
let containsCard =
range.containsCard() ||
(range.commonAncestorNode.closest(CARD_SELECTOR).length >
0 &&
range.startNode.closest(
`${CARD_LEFT_SELECTOR},${CARD_RIGHT_SELECTOR},${UI_SELECTOR}`,
).length === 0);
let isSingle = range.collapsed;
if (!isSingle) {
const { startNode, endNode, startOffset, endOffset } =
range;
const startElement =
startNode.isElement() && !startNode.isCard()
? startNode.children().eq(startOffset)
: startNode;
const endElement =
endNode.isElement() && !endNode.isCard()
? endNode.children().eq(endOffset - 1)
: endNode;
if (
startElement &&
endElement &&
startElement.isCard() &&
startElement.equal(endElement)
) {
isSingle = true;
}
}
card.each((card) => {
const center = card.getCenter();
if (center && center.length > 0) {
let isSelect = selection.containsNode(center[0]);
if (!isSelect && containsCard && selection.focusNode) {
const focusCard = this.engine.card.find(
selection.focusNode,
);
if (focusCard) {
isSingle =
!selection.anchorNode ||
focusCard.root.contains(
selection.anchorNode,
);
if (
isSingle &&
card.root.equal(focusCard.root)
) {
isSelect = true;
}
}
// 找到一次其它的就不需要再去比对了
if (isSelect && isSingle) containsCard = false;
}
const { autoSelected } = card.constructor as CardEntry;
card.select(
isSelect && (!isSingle || autoSelected !== false),
);
}
});
}
if (selectionChangeTimeout) clearTimeout(selectionChangeTimeout);
selectionChangeTimeout = setTimeout(
() => this.handleSelectionChange(),
50,
);
});
change.event.onSelect((event: any) => {
const range = change.range.get();

View File

@ -4,6 +4,7 @@ import { READY_CARD_KEY, READY_CARD_SELECTOR } from '../constants/card';
import Parser from '../parser';
import { EngineInterface } from '../types/engine';
import { $ } from '../node';
import { DATA_ID } from '../constants';
export default class Paste {
protected source: string;
@ -26,77 +27,27 @@ export default class Paste {
}
getDefaultStyle() {
const defaultStyle = [
{
color: tinycolor2(this.engine.container.css('color')).toHex(),
},
{
'background-color': tinycolor2('white').toHex(),
},
{
'font-size': this.engine.container.css('font-size'),
},
];
const defaultStyle = {
color: tinycolor2(this.engine.container.css('color')).toHex(),
'background-color': tinycolor2('white').toHex(),
'font-size': this.engine.container.css('font-size'),
};
return defaultStyle;
}
elementNormalize(fragment: DocumentFragment) {
const defaultStyle = this.getDefaultStyle();
const defautlStyleKeys = Object.keys(defaultStyle);
const { inline } = this.engine;
const nodeApi = this.engine.node;
$(fragment).traverse((node) => {
// 跳过Card
if (node.isCard() || fragment === node.fragment) {
let parent = node.parent();
// 跳过已被删除的节点
if (!parent || node.isCard() || node.fragment === fragment) {
return;
}
// 删除与默认样式一样的样式
if (node.isElement()) {
defaultStyle.forEach((style) => {
const key = Object.keys(style)[0];
const defaultValue = style[key];
let value = node.get<HTMLElement>()?.style[key];
if (value) {
if (/color$/.test(key)) {
value = tinycolor2(value).toHex();
}
if (value === defaultValue) {
node.css(key, '');
}
}
});
//处理后如果不是一个有效的节点就移除包裹
if (!this.schema.getType(node)) nodeApi.unwrap(node);
nodeApi.removeMinusStyle(node, 'text-indent');
if (nodeApi.isList(node)) {
node.css('padding-left', '');
}
// 删除空 style 属性
if (!node.attributes('style')) {
node.removeAttributes('style');
}
// 删除空 span
let first: NodeInterface | null = null;
if (
node.name === 'span' &&
Object.keys(node.attributes()).length === 0 &&
Object.keys(node.css()).length === 0 &&
(node.text().trim() === '' ||
(first =
node.first() &&
first &&
(nodeApi.isMark(first, this.schema) ||
nodeApi.isBlock(first, this.schema))))
) {
nodeApi.unwrap(node);
return;
}
// br 换行改成正常段落
if (nodeApi.isBlock(node, this.schema)) {
this.engine.block.brToBlock(node);
}
} else {
if (node.isText()) {
let text = node.text();
if (/\s/.test(text)) {
text = text.replace(/\s/g, ' ');
@ -120,14 +71,41 @@ export default class Paste {
node.text(text);
}
}
}
});
$(fragment).traverse((node) => {
let parent = node.parent();
// 跳过已被删除的节点
if (!parent || node.fragment === fragment) {
return;
}
const styles = node.css();
defautlStyleKeys.forEach((key) => {
const value = styles[key];
if (!value) return;
if (value === defaultStyle[key]) {
node.css(key, '');
}
});
//处理后如果不是一个有效的节点就移除包裹
if (!this.schema.getType(node)) {
nodeApi.unwrap(node);
return;
}
nodeApi.removeMinusStyle(node, 'text-indent');
if (nodeApi.isList(node)) {
node.css('padding-left', '');
}
// 删除空 style 属性
if (node.attributes('style').trim() === '') {
node.removeAttributes('style');
}
// br 换行改成正常段落
if (nodeApi.isBlock(node, this.schema)) {
this.engine.block.brToBlock(node);
}
// 删除空 span
while (node.name === 'span' && nodeApi.isEmpty(node)) {
parent = node.parent();
node.remove();
if (!parent) return;
node = parent;
}
// 删除 google docs 根节点
// <b style="font-weight:flat;" id="docs-internal-guid-e0280780-7fff-85c2-f58a-6e615d93f1f2">
if (/^docs-internal-guid-/.test(node.attributes('id'))) {
@ -138,12 +116,15 @@ export default class Paste {
if (node.attributes(READY_CARD_KEY)) {
return;
}
const nodeIsBlock = nodeApi.isBlock(node, this.schema);
const nodeIsVoid = nodeApi.isVoid(node, this.schema);
let parentIsBlock = nodeApi.isBlock(parent, this.schema);
// 删除零高度的空行
if (
nodeApi.isBlock(node, this.schema) &&
nodeIsBlock &&
node.attributes('data-type') !== 'p' &&
!nodeApi.isVoid(node, this.schema) &&
!nodeApi.isBlock(parent, this.schema) &&
!nodeIsVoid &&
!parentIsBlock &&
//!node.isSolid() &&
nodeApi.html(node) === ''
) {
@ -154,19 +135,24 @@ export default class Paste {
if (node.attributes('data-type') === 'p') {
node.removeAttributes('data-type');
}
if (nodeApi.isBlock(node, this.schema) && parent?.name === 'p') {
nodeApi.unwrap(node);
if (nodeIsBlock && parent?.name === 'p') {
nodeApi.unwrap(parent);
parent = node.parent();
if (parent?.fragment === fragment) parent = undefined;
parentIsBlock = parent
? nodeApi.isBlock(parent, this.schema)
: false;
}
const parentIsList = parent ? nodeApi.isList(parent) : false;
// 补齐 ul 或 ol
if (node.name === 'li' && parent && !nodeApi.isList(parent)) {
if (node.name === 'li' && parent && !parentIsList) {
const ul = $('<ul />');
node.before(ul);
ul.append(node);
return;
}
// 补齐 li
if (node.name !== 'li' && parent && nodeApi.isList(parent)) {
if (node.name !== 'li' && parentIsList) {
const li = $('<li />');
node.before(li);
li.append(node);
@ -202,14 +188,14 @@ export default class Paste {
return;
}
// p 改成 li
if (node.name === 'p' && parent && nodeApi.isList(parent)) {
if (node.name === 'p' && parentIsList) {
nodeApi.replace(node, $('<li />'));
return;
}
// 处理空 Block
if (
nodeApi.isBlock(node, this.schema) &&
!nodeApi.isVoid(node, this.schema) &&
nodeIsBlock &&
!nodeIsVoid &&
nodeApi.html(node).trim() === ''
) {
// <p></p> to <p><br /></p>
@ -221,7 +207,7 @@ export default class Paste {
}
}
// <li><p>foo</p></li>
if (nodeApi.isBlock(node, this.schema) && parent?.name === 'li') {
if (nodeIsBlock && parent?.name === 'li') {
// <li><p><br /></p></li>
if (
node.children().length === 1 &&
@ -235,9 +221,10 @@ export default class Paste {
return;
}
if (
!nodeIsBlock &&
nodeApi.isInline(node) &&
!node.isCard() &&
!nodeApi.isVoid(node, this.schema)
!nodeIsVoid
) {
const isVoid = node
.allChildren()
@ -334,7 +321,7 @@ export default class Paste {
$(fragment).traverse((node) => {
if (node.fragment === fragment) return;
if (node.length > 0 && node.parent())
if (node.length > 0 && node[0].parentNode)
this.engine.trigger('paste:each', node);
// 删除非block节点的换行 \r\n\r\n<span
if (node.isText()) {
@ -348,13 +335,6 @@ export default class Paste {
)
node.remove();
}
}
});
$(fragment).traverse((node) => {
if (node.fragment === fragment) return;
this.engine.trigger('paste:each-after', node);
if (node.isText()) {
const text = node.text();
const match = /((\n)+)/.exec(text);
if (match && !text.endsWith('\n') && !text.startsWith('\n')) {
const nextReg = node.get<Text>()!.splitText(match.index);
@ -364,13 +344,14 @@ export default class Paste {
}
}
// 删除包含Card的 pre 标签
if (
else if (
node.name === 'pre' &&
node.find(READY_CARD_SELECTOR).length > 0
) {
nodeApi.unwrap(node);
}
});
this.engine.trigger('paste:each-after', $(fragment));
const node = nodeApi.normalize($(fragment));
if (node.fragment) fragment = node.fragment;
@ -391,14 +372,12 @@ export default class Paste {
nodeApi.unwrap(first);
}
fragmentNode = $(fragment);
fragmentNode.each((_, index) => {
const children = fragmentNode.eq(index);
children?.find('ul,ol').each((_, index) => {
const child = children.eq(index);
if (child && nodeApi.isList(child)) {
this.engine.list.addStart(child);
}
});
const children = fragmentNode.find('ul,ol');
children.each((_, index) => {
const child = children.eq(index);
if (child && nodeApi.isList(child)) {
this.engine.list.addStart(child);
}
});
this.engine.nodeId.generateAll($(fragment), true);
return fragment;

View File

@ -244,9 +244,14 @@ class ChangeRange implements ChangeRangeInterface {
) {
endNode.append('<br />');
}
const startChildren = startNode.children();
// 列表节点没有子节点
if (node.isList(startNode) && startNode.children().length === 0) {
if (
node.isList(startNode) &&
(startChildren.length === 0 || startChildren[0].nodeName === 'BR')
) {
const newNode = $('<p><br /></p>');
this.engine.nodeId.create(newNode);
startNode.before(newNode);
startNode.remove();
startNode = newNode;

View File

@ -3,7 +3,7 @@ import Parser from './parser';
import { ClipboardInterface } from './types/clipboard';
import { EditorInterface, EngineInterface } from './types/engine';
import { RangeInterface } from './types/range';
import { getWindow, isEngine, isSafari } from './utils';
import { isEngine, isSafari } from './utils';
import { $ } from './node';
import Range from './range';
import { NodeInterface } from './types';
@ -145,7 +145,7 @@ export default class Clipboard implements ClipboardInterface {
root.name === '#text' ? [document.createElement('span')] : [];
card = root.closest(`[${CARD_KEY}]`, (node) => {
if ($(node).isEditable()) return;
if (node.nodeType === getWindow().Node.ELEMENT_NODE) {
if (node.nodeType === Node.ELEMENT_NODE) {
const display = window
.getComputedStyle(node as Element)
.getPropertyValue('display');
@ -170,60 +170,57 @@ export default class Clipboard implements ClipboardInterface {
} else {
// 修复自定义列表选择范围
let customizeStartItem: NodeInterface | undefined;
if (range.startNode.isText()) {
const li = range.startNode.closest('li');
const li = range.startNode.closest('li');
if (li && node.isCustomize(li)) {
const endLi = range.endNode.closest('li');
if (
!li.equal(endLi) ||
(list.isLast(range) && list.isFirst(range))
) {
if (range.startOffset === 0) {
const ul = li.parent();
const index = li.getIndex();
if (ul)
range.setStart(ul, index < 0 ? 0 : index);
} else {
const ul = li.parent();
// 选在列表项靠后的节点,把剩余节点拼接成完成的列表项
const selection = range.createSelection();
const rightNode = selection.getNode(
li,
'center',
true,
);
selection.anchor?.remove();
selection.focus?.remove();
if (isEngine(this.editor))
this.editor.change.combinText();
if (rightNode.length > 0) {
let isRemove = false;
rightNode.each((_, index) => {
const item = rightNode.eq(index);
if (!isRemove && item?.name === 'li') {
isRemove = true;
return;
}
if (isRemove) item?.remove();
});
const card = li.first();
const component = card
? this.editor.card.find(card)
: undefined;
if (component) {
customizeStartItem = rightNode;
this.editor.list.addCardToCustomize(
customizeStartItem,
component.name,
component.getValue(),
);
if (ul)
node.wrap(
customizeStartItem,
ul?.clone(),
);
if (li && node.isCustomize(li)) {
const endLi = range.endNode.closest('li');
if (
!li.equal(endLi) ||
(list.isLast(range) && list.isFirst(range))
) {
if (list.isFirst(range)) {
const ul = li.parent();
const index = li.getIndex();
if (ul) range.setStart(ul, index < 0 ? 0 : index);
} else {
const ul = li.parent();
// 选在列表项靠后的节点,把剩余节点拼接成完成的列表项
const selection = range.createSelection();
const rightNode = selection.getNode(
li,
'center',
true,
);
selection.anchor?.remove();
selection.focus?.remove();
if (isEngine(this.editor))
this.editor.change.combinText();
if (rightNode.length > 0) {
let isRemove = false;
rightNode.each((_, index) => {
const item = rightNode.eq(index);
if (!isRemove && item?.name === 'li') {
isRemove = true;
return;
}
if (isRemove) item?.remove();
});
const card = li.first();
const component = card
? this.editor.card.find(card)
: undefined;
if (component) {
customizeStartItem = rightNode;
this.editor.list.addCardToCustomize(
customizeStartItem,
component.name,
component.getValue(),
);
if (ul)
node.wrap(
customizeStartItem,
ul?.clone(),
);
}
}
}
@ -235,15 +232,13 @@ export default class Clipboard implements ClipboardInterface {
contents.prepend(customizeStartItem[0]);
}
const listMergeBlocks: NodeInterface[] = [];
contents.childNodes.forEach((child) => {
let childElement = $(child);
if (childElement.name !== 'li') return;
contents.querySelectorAll('li').forEach((child) => {
const childElement = $(child);
const dataId = childElement.attributes(DATA_ID);
if (!dataId) return;
const curentElement = document.querySelector(
`[${DATA_ID}=${dataId}]`,
);
const curentElement = this.editor.container
.get<HTMLElement>()
?.querySelector(`[${DATA_ID}=${dataId}]`);
// 补充自定义列表丢失的卡片
if (
node.isCustomize(childElement) &&

View File

@ -9,21 +9,14 @@ import { HotkeyInterface } from './types/hotkey';
class Hotkey implements HotkeyInterface {
private engine: EngineInterface;
private disabled: boolean = false;
#matchTimeout?: NodeJS.Timeout;
constructor(engine: EngineInterface) {
this.engine = engine;
//绑定事件
this.engine.container.on('keydown', (event) => this.trigger(event));
}
/**
*
* @param e
*/
trigger(e: KeyboardEvent) {
//禁用快捷键不处理
if (this.disabled) {
return;
}
match(e: KeyboardEvent) {
//遍历插件
Object.keys(this.engine.plugin.components).every((name) => {
const plugin = this.engine.plugin.components[name];
@ -81,6 +74,21 @@ class Hotkey implements HotkeyInterface {
});
}
/**
*
* @param e
*/
trigger(e: KeyboardEvent) {
//禁用快捷键不处理
if (this.disabled) {
return;
}
if (this.#matchTimeout) {
clearTimeout(this.#matchTimeout);
}
this.#matchTimeout = setTimeout(() => this.match(e), 50);
}
/**
*
*/

View File

@ -8,12 +8,13 @@ import { EditorInterface, EngineInterface } from '../types/engine';
import { InlineModelInterface } from '../types/inline';
import { NodeInterface } from '../types/node';
import { RangeInterface } from '../types/range';
import { getDocument, getWindow, isEngine } from '../utils';
import { getDocument, isEngine } from '../utils';
import { Backspace, Left, Right } from './typing';
import { $ } from '../node';
import { isNode } from '../node/utils';
import { isInlinePlugin } from '../plugin/inline';
import { isRangeInterface } from '../range';
import { SchemaInterface } from 'src';
class Inline implements InlineModelInterface {
private editor: EditorInterface;
@ -197,7 +198,7 @@ class Inline implements InlineModelInterface {
this.split(safeRange);
let { commonAncestorNode } = safeRange;
if (
commonAncestorNode.type === getWindow().Node.TEXT_NODE ||
commonAncestorNode.type === Node.TEXT_NODE ||
node.isMark(commonAncestorNode)
) {
commonAncestorNode = commonAncestorNode.parent()!;
@ -808,13 +809,13 @@ class Inline implements InlineModelInterface {
let startNode = sc;
let endNode = ec;
if (sc.nodeType === getWindow().Node.ELEMENT_NODE) {
if (sc.nodeType === Node.ELEMENT_NODE) {
if (sc.childNodes[so]) {
startNode = sc.childNodes[so] || sc;
}
}
if (ec.nodeType === getWindow().Node.ELEMENT_NODE) {
if (ec.nodeType === Node.ELEMENT_NODE) {
if (eo > 0 && ec.childNodes[eo - 1]) {
endNode = ec.childNodes[eo - 1] || sc;
}
@ -999,7 +1000,7 @@ class Inline implements InlineModelInterface {
}
}
flat(node: NodeInterface | RangeInterface) {
flat(node: NodeInterface | RangeInterface, schema?: SchemaInterface) {
if (isRangeInterface(node)) {
const selection = node.shrinkToElementNode().createSelection();
const inlines = this.findInlines(node);
@ -1014,19 +1015,28 @@ class Inline implements InlineModelInterface {
const nodeApi = this.editor.node;
const markApi = this.editor.mark;
//当前节点是 inline 节点inline 节点不允许嵌套、不允许放入mark节点
if (nodeApi.isInline(node) && node.name !== 'br') {
if (nodeApi.isInline(node, schema) && node.name !== 'br') {
const parentInline = this.closest(node);
//不允许嵌套
if (!parentInline.equal(node) && nodeApi.isInline(parentInline)) {
if (
!parentInline.equal(node) &&
nodeApi.isInline(parentInline, schema)
) {
nodeApi.unwrap(node);
}
//不允许放入mark
else {
const parentMark = markApi.closest(node);
if (!parentMark.equal(node) && nodeApi.isMark(parentMark)) {
let parentMark: NodeInterface | undefined =
markApi.closest(node);
while (
parentMark &&
!parentMark.equal(node) &&
nodeApi.isMark(parentMark, schema)
) {
const cloneMark = parentMark.clone();
const inlineMark = node.clone();
parentMark.children().each((markChild) => {
const children = parentMark.children();
children.each((markChild) => {
// 零宽字符的文本跳过
if (
markChild.nodeType === 3 &&
@ -1034,17 +1044,21 @@ class Inline implements InlineModelInterface {
) {
return;
}
if (node.equal(markChild)) {
nodeApi.wrap(
nodeApi.replace(node, cloneMark),
if ((node as NodeInterface).equal(markChild)) {
node = nodeApi.wrap(
nodeApi.replace(
node as NodeInterface,
cloneMark,
),
inlineMark,
);
this.repairBoth(inlineMark);
this.repairBoth(node);
} else {
nodeApi.wrap(markChild, cloneMark);
}
});
nodeApi.unwrap(parentMark);
parentMark = markApi.closest(node);
}
}
}

View File

@ -7,7 +7,7 @@ import {
RangeInterface,
} from '../types';
import { ListInterface, ListModelInterface } from '../types/list';
import { getDocument, getWindow, isEngine, removeUnit } from '../utils';
import { getDocument, isEngine, removeUnit } from '../utils';
import { Enter, Backspace } from './typing';
import { $ } from '../node';
import { isNode } from '../node/utils';
@ -716,12 +716,12 @@ class List implements ListModelInterface {
return;
}
//文本
if (child.type === getWindow().Node.TEXT_NODE) {
if (child.type === Node.TEXT_NODE) {
if (child.text() !== '') return;
child = child.prev();
}
//节点
else if (child.type === getWindow().Node.ELEMENT_NODE) {
else if (child.type === Node.ELEMENT_NODE) {
if (!nodeApi.isMark(child) || child.text() !== '')
return;
child = node.prev();

View File

@ -7,7 +7,7 @@ import {
} from '../constants';
import { EditorInterface, NodeInterface, RangeInterface } from '../types';
import { MarkInterface, MarkModelInterface } from '../types/mark';
import { getDocument, getWindow, isEngine } from '../utils';
import { getDocument, isEngine } from '../utils';
import { Backspace } from './typing';
import { $ } from '../node';
import { isNode } from '../node/utils';
@ -541,7 +541,7 @@ class Mark implements MarkModelInterface {
}
}
};
if (at.nodeType === getWindow().Node.TEXT_NODE) {
if (at.nodeType === Node.TEXT_NODE) {
const { textContent } = at;
atText = textContent!;
atTextLen = atText.length;
@ -758,7 +758,7 @@ class Mark implements MarkModelInterface {
if (!node.isMark(mark)) return;
let { commonAncestorNode } = safeRange;
if (commonAncestorNode.type === getWindow().Node.TEXT_NODE) {
if (commonAncestorNode.type === Node.TEXT_NODE) {
commonAncestorNode = commonAncestorNode.parent()!;
}
const card = this.editor.card.find(commonAncestorNode, true);
@ -849,7 +849,7 @@ class Mark implements MarkModelInterface {
if (!isEditable) {
this.split(safeRange);
commonAncestorNode = safeRange.commonAncestorNode;
if (commonAncestorNode.type === getWindow().Node.TEXT_NODE) {
if (commonAncestorNode.type === Node.TEXT_NODE) {
commonAncestorNode = commonAncestorNode.parent()!;
}
nodes[0] = commonAncestorNode;
@ -1135,7 +1135,7 @@ class Mark implements MarkModelInterface {
}
let { commonAncestorNode } = safeRange;
if (commonAncestorNode.type === getWindow().Node.TEXT_NODE) {
if (commonAncestorNode.type === Node.TEXT_NODE) {
commonAncestorNode = commonAncestorNode.parent()!;
}
const card = this.editor.card.find(commonAncestorNode, true);
@ -1153,7 +1153,7 @@ class Mark implements MarkModelInterface {
if (!isEditable) {
this.split(safeRange, safeRange.collapsed ? removeMark : undefined);
commonAncestorNode = safeRange.commonAncestorNode;
if (commonAncestorNode.type === getWindow().Node.TEXT_NODE) {
if (commonAncestorNode.type === Node.TEXT_NODE) {
commonAncestorNode = commonAncestorNode.parent()!;
}
nodes[0] = commonAncestorNode;
@ -1405,14 +1405,11 @@ class Mark implements MarkModelInterface {
const eo = cloneRange.endOffset;
let startNode = sc;
let endNode = ec;
if (
sc.nodeType === getWindow().Node.ELEMENT_NODE &&
sc.childNodes[so]
) {
if (sc.nodeType === Node.ELEMENT_NODE && sc.childNodes[so]) {
startNode = sc.childNodes[so] || sc;
}
if (
ec.nodeType === getWindow().Node.ELEMENT_NODE &&
ec.nodeType === Node.ELEMENT_NODE &&
eo > 0 &&
ec.childNodes[eo - 1]
) {
@ -1433,10 +1430,7 @@ class Mark implements MarkModelInterface {
const findNodes = (node: NodeInterface) => {
let nodes: Array<NodeInterface> = [];
while (node) {
if (
node.type === getWindow().Node.ELEMENT_NODE &&
node.isEditable()
) {
if (node.type === Node.ELEMENT_NODE && node.isEditable()) {
break;
}
if (

View File

@ -13,9 +13,7 @@ import {
toCamelCase,
getStyleMap,
getComputedStyle,
getAttrMap,
getDocument,
getWindow,
} from '../utils';
import { Path } from 'sharedb';
import {
@ -49,25 +47,24 @@ class NodeEntry implements NodeInterface {
constructor(nodes: Node | NodeList | Array<Node>, context?: Context) {
if (isNode(nodes)) {
if (nodes.nodeType === getWindow().Node.DOCUMENT_FRAGMENT_NODE) {
if (nodes.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
this.fragment = nodes as DocumentFragment;
}
nodes = [nodes];
}
for (let i = 0; i < nodes.length; i++) {
this[i] = nodes[i];
this.events[i] = new DOMEvent(); // 初始化事件对象
}
nodes.forEach((node, index) => {
this[index] = node;
this.events[index] = new DOMEvent(); // 初始化事件对象
});
this.length = nodes.length;
if (this.length > 0) {
if (this[0]) {
this.document = getDocument(context);
this.context = context;
this.name = this[0].nodeName ? this[0].nodeName.toLowerCase() : '';
this.name = this[0].nodeName.toLowerCase();
this.type = this[0].nodeType;
this.window = getWindow(this[0]);
this.window = this.document.defaultView || window;
}
}
@ -77,7 +74,7 @@ class NodeEntry implements NodeInterface {
* @param selector
*/
isMatchesSelector(element: ElementInterface, selector: string) {
if (element.nodeType !== getWindow().Node.ELEMENT_NODE || !selector) {
if (element.nodeType !== Node.ELEMENT_NODE || !selector) {
return false;
}
const defaultMatches = (element: Element, selector: string) => {
@ -107,10 +104,13 @@ class NodeEntry implements NodeInterface {
each(
callback: (node: Node, index: number) => boolean | void,
): NodeInterface {
for (let i = 0; i < this.length; i++) {
if (callback(this[i], i) === false) {
let i = 0,
node;
while ((node = this[i])) {
if (callback(node, i) === false) {
break;
}
i++;
}
return this;
}
@ -132,7 +132,7 @@ class NodeEntry implements NodeInterface {
* @return {boolean}
*/
isElement(): boolean {
return this.type === getWindow().Node.ELEMENT_NODE;
return this.type === Node.ELEMENT_NODE;
}
/**
@ -140,7 +140,7 @@ class NodeEntry implements NodeInterface {
* @return {boolean}
*/
isText(): boolean {
return this.type === getWindow().Node.TEXT_NODE;
return this.type === Node.TEXT_NODE;
}
/**
@ -167,7 +167,11 @@ class NodeEntry implements NodeInterface {
* @returns
*/
isEditableCard() {
return this.find(EDITABLE_SELECTOR).length > 0;
return (
this.attributes(DATA_ELEMENT) === EDITABLE ||
(this.isElement() &&
!!(this[0] as Element).querySelector(EDITABLE_SELECTOR))
);
}
/**
@ -224,7 +228,7 @@ class NodeEntry implements NodeInterface {
let prev = this.get()?.previousSibling;
let index = 0;
while (prev && prev.nodeType === getWindow().Node.ELEMENT_NODE) {
while (prev && prev.nodeType === Node.ELEMENT_NODE) {
index++;
prev = prev.previousSibling;
}
@ -372,8 +376,8 @@ class NodeEntry implements NodeInterface {
return false;
}
if (
this.get()!.nodeType === getWindow().Node.DOCUMENT_NODE &&
domNode?.nodeType !== getWindow().Node.DOCUMENT_NODE
this.get()!.nodeType === Node.DOCUMENT_NODE &&
domNode?.nodeType !== Node.DOCUMENT_NODE
) {
return true;
}
@ -395,7 +399,9 @@ class NodeEntry implements NodeInterface {
*/
find(selector: string): NodeInterface {
if (this.length > 0 && this.isElement()) {
const nodeList = this.get<Element>()?.querySelectorAll(selector);
const nodeList = (
this.fragment ? this.fragment : this.get<Element>()
)?.querySelectorAll(selector);
return new NodeEntry(nodeList || []);
}
return new NodeEntry([]);
@ -526,8 +532,18 @@ class NodeEntry implements NodeInterface {
val?: string | number,
): NodeEntry | { [k: string]: string } | string {
if (key === undefined) {
const element = this.clone(false).get<Element>();
return getAttrMap(element?.outerHTML || '');
const element = this.get<Element>();
if (!element) return {};
const attrs = {};
const elementAttributes = element.attributes;
let i = 0,
item = null;
while ((item = elementAttributes[i])) {
const { name, value } = item;
attrs[name] = value;
i++;
}
return attrs;
}
if (typeof key === 'object') {
@ -560,6 +576,7 @@ class NodeEntry implements NodeInterface {
*/
removeAttributes(key: string): NodeInterface {
this.each((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
const element = <Element>node;
element.removeAttribute(key);
});
@ -573,13 +590,14 @@ class NodeEntry implements NodeInterface {
*/
hasClass(className: string): boolean {
if (this.length === 0) return false;
const element = this.get<Element>()!;
if (element.classList) {
for (let i = 0; i < element.classList.length; i++) {
if (element.classList[i] === className) {
return true;
}
}
const element = this.get<Element>();
if (!element) return false;
let i = 0,
name = null;
const classList = element.classList || {};
while ((name = classList[i])) {
if (name === className) return true;
i++;
}
return false;
}
@ -689,8 +707,17 @@ class NodeEntry implements NodeInterface {
html(html: string): NodeEntry;
html(html?: string): NodeEntry | string {
if (html !== undefined) {
const children = $(html);
this.each((node) => {
(node as Element).innerHTML = html;
let child = node.firstChild;
while (child) {
const next = child.nextSibling;
node.removeChild(child);
child = next;
}
children.forEach((child) => {
(node as Element).append(child.cloneNode(true));
});
});
return this;
}
@ -772,12 +799,8 @@ class NodeEntry implements NodeInterface {
this.each((node) => {
let child = node.firstChild;
while (child) {
if (!node.parentNode) {
return;
}
const next = child.nextSibling;
child.parentNode?.removeChild(child);
node.removeChild(child);
child = next;
}
});
@ -809,9 +832,10 @@ class NodeEntry implements NodeInterface {
*/
prepend(selector: Selector): NodeInterface {
const nodes = $(selector, this.context);
const isClone = typeof selector === 'string' && /<.+>/.test(selector);
this.each((node) => {
for (let i = nodes.length - 1; i >= 0; i--) {
const child = nodes[i];
const child = isClone ? nodes[i].cloneNode(true) : nodes[i];
if (node.firstChild) {
node.insertBefore(child, node.firstChild);
} else {
@ -829,11 +853,12 @@ class NodeEntry implements NodeInterface {
*/
append(selector: Selector): NodeInterface {
const nodes = $(selector, this.context);
const isClone = typeof selector === 'string' && /<.+>/.test(selector);
this.each((node) => {
for (let i = 0; i < nodes.length; i++) {
const child = nodes[i];
const child = isClone ? nodes[i].cloneNode(true) : nodes[i];
if (typeof selector === 'string') {
node.appendChild(child.cloneNode(true));
node.appendChild(child);
} else {
node.appendChild(child);
}
@ -848,10 +873,14 @@ class NodeEntry implements NodeInterface {
* @return
*/
before(selector: Selector): NodeInterface {
const nodes = $(selector, this.context);
const isClone = typeof selector === 'string' && /<.+>/.test(selector);
this.each((node) => {
const nodes = $(selector, this.context);
const parentNode = node.parentNode;
if (!parentNode) return;
nodes.forEach((child) => {
node.parentNode?.insertBefore(child, node);
if (isClone) child = child.cloneNode(true);
parentNode.insertBefore(child, node);
node = child;
});
});
@ -864,14 +893,18 @@ class NodeEntry implements NodeInterface {
* @return
*/
after(selector: Selector): NodeInterface {
const nodes = $(selector, this.context);
const isClone = typeof selector === 'string' && /<.+>/.test(selector);
this.each((node) => {
const nodes = $(selector, this.context);
const parentNode = node.parentNode;
if (!parentNode) return;
nodes.forEach((child) => {
if (isClone) child = child.cloneNode(true);
if (node.nextSibling) {
node.parentNode?.insertBefore(child, node.nextSibling);
parentNode.insertBefore(child, node.nextSibling);
node = child;
} else {
node.parentNode?.appendChild(child);
parentNode.appendChild(child);
}
});
});
@ -885,10 +918,13 @@ class NodeEntry implements NodeInterface {
*/
replaceWith(selector: Selector): NodeInterface {
const newNodes: Array<Node> = [];
const nodes = $(selector, this.context);
const isClone = typeof selector === 'string' && /<.+>/.test(selector);
this.each((node) => {
const nodes = $(selector, this.context);
const newNode = nodes[0];
node.parentNode?.replaceChild(newNode, node);
const parentNode = node.parentNode;
if (!parentNode) return;
const newNode = isClone ? nodes[0].cloneNode(true) : nodes[0];
parentNode.replaceChild(newNode, node);
newNodes.push(newNode);
});
return new NodeEntry(newNodes);
@ -914,7 +950,7 @@ class NodeEntry implements NodeInterface {
}
if (result !== true) {
if (child.isEditableCard() && includeEditableCard) {
if (includeEditableCard && child.isEditableCard()) {
const editableElements = child.find(EDITABLE_SELECTOR);
editableElements.each((_, index) => {
const editableElement = editableElements.eq(index);
@ -1014,7 +1050,7 @@ class NodeEntry implements NodeInterface {
inViewport(node: NodeInterface, view: NodeInterface) {
let viewNode = null;
if (view.type !== getWindow().Node.ELEMENT_NODE) {
if (view.type !== Node.ELEMENT_NODE) {
if (!view.document) return false;
viewNode = view.document.createElement('span');
if (view.next()) {
@ -1045,7 +1081,7 @@ class NodeEntry implements NodeInterface {
if (typeof view.document?.body.scrollIntoView === 'function') {
let viewElement = null;
if (
view.type !== getWindow().Node.ELEMENT_NODE ||
view.type !== Node.ELEMENT_NODE ||
view.name.toLowerCase() === 'br'
) {
viewElement = view.document.createElement('span');

View File

@ -49,14 +49,10 @@ class Event implements EventInterface {
const listeners = this.listeners[eventType];
if (listeners) {
let result;
for (var i = 0; i < listeners.length; i++) {
result = listeners[i](...args);
if (result === false) {
break;
}
}
listeners.every((listener) => {
result = listener(...args);
return result !== false;
});
return result;
}

View File

@ -41,6 +41,8 @@ export const uuid = (len: number, radix: number = 16): string => {
return uuid.join('');
};
const valueCaches = new Map<string, string>();
export default (
value: string | NodeInterface | Node,
unique: boolean = true,
@ -48,24 +50,27 @@ export default (
let prefix = '';
if (isNode(value)) value = $(value);
if (isNodeEntry(value)) {
const attributes = value.attributes();
const styles = attributes['style'];
delete attributes['style'];
const attributes = value.attributes() || {};
delete attributes[DATA_ID];
prefix = value.name.substring(0, 1);
value = `${value.name}_${Object.keys(attributes || {}).join(
',',
)}_${Object.values(attributes || {}).join(',')}_${Object.keys(
styles || {},
).join(',')}_${Object.values(styles || {}).join(',')}`;
value = `${value.name}_${JSON.stringify(attributes)}`;
}
const md5Value = md5(value);
let hash =
prefix + md5Value.substr(0, 4) + md5Value.substr(md5Value.length - 3);
const cachePerfix = valueCaches.get(value);
if (!cachePerfix) {
const md5Value = md5(value);
prefix =
prefix +
md5Value.substr(0, 4) +
md5Value.substr(md5Value.length - 3);
valueCaches.set(value, prefix);
} else {
prefix = cachePerfix;
}
const hash = prefix;
if (unique) {
const counter = _counters[hash] || 0;
_counters[hash] = counter + 1;
hash = `${hash}-${uuid(8, 48 + _counters[hash])}`;
return `${hash}-${uuid(8, 48 + _counters[hash])}`;
}
return hash;

View File

@ -68,14 +68,12 @@ class NodeId implements NodeIdInterface {
if (isNodeEntry(root) && root.fragment) {
root = root.fragment;
}
const element = (isNode(root) ? root : root.get<Element>()) as Element;
if (element.nodeType === Node.TEXT_NODE) return;
const nodes =
(isNode(root) ? root : root.get<Element>())?.querySelectorAll(
tagNames,
) || [];
nodes.forEach((child) => {
const node = $(child);
const node = isNode(root) ? root : root.get<Node>();
if (!node || node.nodeType === Node.TEXT_NODE) return;
const nodes = (isNode(root) ? $(root) : root)?.find(tagNames);
nodes.each((_, index) => {
const node = nodes.eq(index);
if (!node) return;
// 有ID不再生成
if (!force && node.attributes(DATA_ID)) return;
@ -95,7 +93,8 @@ class NodeId implements NodeIdInterface {
// 不符合规则
const nodeRules = rules[node.name];
if (
(nodeRules && nodeRules.length === 0) ||
!nodeRules ||
nodeRules.length === 0 ||
!nodeRules.some((rule) =>
this.editor.schema.checkNode(
node as NodeInterface,

View File

@ -21,7 +21,7 @@ import {
READY_CARD_KEY,
READY_CARD_SELECTOR,
} from '../constants';
import { getDocument, getStyleMap, getWindow, isEngine } from '../utils';
import { getDocument, getStyleMap, isEngine } from '../utils';
import $ from './query';
import getHashId from './hash';
import { isNode, isNodeEntry } from './utils';
@ -168,9 +168,9 @@ class NodeModel implements NodeModelInterface {
if (childNodes.length === 0) return true;
for (let i = 0; i < childNodes.length; i++) {
const child = childNodes[i];
if (child.nodeType === getWindow().Node.TEXT_NODE) {
if (child.nodeType === Node.TEXT_NODE) {
if (child['data'].replace(/\u200b/g, '') !== '') return false;
} else if (child.nodeType === getWindow().Node.ELEMENT_NODE) {
} else if (child.nodeType === Node.ELEMENT_NODE) {
if (
child.nodeName.toLowerCase() === 'li' &&
!this.editor.list.isEmptyItem($(child))
@ -446,8 +446,12 @@ class NodeModel implements NodeModelInterface {
* @param source
* @param target
*/
replace(source: NodeInterface, target: NodeInterface) {
const clone = this.clone(target, false, false);
replace(
source: NodeInterface,
target: NodeInterface,
copyId: boolean = false,
) {
const clone = this.clone(target, false, copyId);
let childNode =
this.isCustomize(source) &&
source.name === 'li' &&
@ -499,7 +503,6 @@ class NodeModel implements NodeModelInterface {
if (!isEngine(editor)) return;
const { change, block, schema, mark } = editor;
range = range || change.range.get();
const nodeApi = editor.node;
const { startNode, startOffset } = range
.cloneRange()
.shrinkToTextNode();
@ -512,8 +515,8 @@ class NodeModel implements NodeModelInterface {
//零宽字符前面还有其它字符。或者节点前面还有节点不能是inline节点。或者前面没有节点了并且父级不是inline节点
if (
text.length > 1 ||
(prev && !nodeApi.isInline(prev)) ||
(!prev && parent && !nodeApi.isInline(parent))
(prev && !this.isInline(prev)) ||
(!prev && parent && !this.isInline(parent))
) {
startNode
.get<Text>()!
@ -534,8 +537,16 @@ class NodeModel implements NodeModelInterface {
}
}
if (nodeApi.isBlock(node)) {
const splitNode = block.split(range);
if (this.isBlock(node)) {
// 如果当前光标位置的block节点是空节点就不用分割
const { commonAncestorNode } = range;
let splitNode = null;
if (
this.isBlock(commonAncestorNode) &&
this.isEmpty(commonAncestorNode)
) {
splitNode = commonAncestorNode;
} else splitNode = block.split(range);
let blockNode = block.closest(
range.startNode.isEditable()
? range
@ -683,9 +694,12 @@ class NodeModel implements NodeModelInterface {
* @param style
*/
removeMinusStyle(node: NodeInterface, style: string) {
if (this.isBlock(node)) {
const val = parseInt(node.css(style), 10) || 0;
if (val < 0) node.css(style, '');
if (node.isElement()) {
const styles = node.css();
if (styles[style]) {
const val = parseInt(styles[style] || '0', 10) || 0;
if (val < 0) node.css(style, '');
}
}
}
@ -835,29 +849,29 @@ class NodeModel implements NodeModelInterface {
}
this.removeSide(cloneNode);
block.flat(cloneNode, $(rootElement || []));
const children = cloneNode.children().toArray();
if (
cloneNode.name === 'p' &&
cloneNode
.children()
.toArray()
.filter((node) => !node.isCursor()).length === 0
children.filter((node) => !node.isCursor()).length === 0
) {
cloneNode.append($('<br />'));
}
if (
this.editor.node.isBlock(cloneNode) &&
this.isEmptyWithTrim(cloneNode)
) {
cloneNode.html('<br />');
}
}
const children = childNode.children().toArray();
if (
childNode.name === 'p' &&
childNode
.children()
.toArray()
.filter((node) => !node.isCursor()).length === 0
children.filter((node) => !node.isCursor()).length === 0
) {
childNode.append($('<br />'));
}
if (
this.editor.node.isBlock(childNode) &&
childNode.children().length === 1 &&
childNode.first()?.isText() &&
this.isEmptyWithTrim(childNode)
) {
childNode.html('<br />');
@ -956,7 +970,7 @@ class NodeModel implements NodeModelInterface {
removeZeroWidthSpace(node: NodeInterface) {
node.traverse((child) => {
const node = child[0];
if (node.nodeType !== getWindow().Node.TEXT_NODE) {
if (node.nodeType !== Node.TEXT_NODE) {
return;
}
const text = node.nodeValue;
@ -968,7 +982,7 @@ class NodeModel implements NodeModelInterface {
if (
text.charCodeAt(1) === 0x200b &&
next &&
next.nodeType === getWindow().Node.ELEMENT_NODE &&
next.nodeType === Node.ELEMENT_NODE &&
[ANCHOR, FOCUS, CURSOR].indexOf(
(<Element>next).getAttribute(DATA_ELEMENT) || '',
) >= 0

View File

@ -1,7 +1,11 @@
import { Selector, Context } from '../types/node';
import { getDocument, getWindow } from '../utils/node';
import { getDocument } from '../utils/node';
import { isNode, isNodeEntry, isNodeList } from '../node/utils';
/**
* selector创建的node
*/
const nodeCaches = new Map<string, Element>();
/**
*
* @param selector
@ -21,6 +25,20 @@ function domParser(
const isTd = selector.indexOf('<td') === 0;
//替换注释
selector = selector.trim().replace(/<!--[^>]*-->/g, '');
const cacheNode = nodeCaches.get(selector);
if (cacheNode) {
if (isTr) {
const tbody = cacheNode.querySelector('tbody');
return tbody ? tbody.cloneNode(true).childNodes : [];
}
if (isTd) {
const tr = cacheNode.querySelector('tr');
return tr ? tr.cloneNode(true).childNodes : [];
}
return cacheNode.cloneNode(true).childNodes;
}
/**
* trtd trtd标签这里需要补充 table
*/
@ -40,7 +58,7 @@ function domParser(
//创建一个空节点,用来包裹需要生成的节点
const container = getDocument().createElement('div');
container.innerHTML = selector;
nodeCaches.set(selector, container.cloneNode(true) as Element);
if (isTr) {
const tbody = container.querySelector('tbody');
return tbody ? tbody.childNodes : [];
@ -70,10 +88,7 @@ function domParser(
return nodes;
}
// 片段
if (
isNode(selector) &&
selector.nodeType === getWindow().Node.DOCUMENT_FRAGMENT_NODE
) {
if (isNode(selector) && selector.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
const nodes: Node[] = [];
let node = selector.firstChild;
while (node) {

View File

@ -1,6 +1,6 @@
import Node from './entry';
import { NodeInterface, Selector, Context, NodeEntry } from '../types/node';
import { getDocument, getWindow } from '../utils/node';
import { getDocument } from '../utils/node';
import Parse from './parse';
import { isNode } from './utils';
@ -20,7 +20,7 @@ export default (
const entry = new (clazz || Node)(nodes, context ? context : undefined);
if (
isNode(selector) &&
selector.nodeType === getWindow().Node.DOCUMENT_FRAGMENT_NODE
selector.nodeType === window.Node.DOCUMENT_FRAGMENT_NODE
)
entry.fragment = selector as DocumentFragment;
return entry;

View File

@ -10,7 +10,7 @@ import {
TargetOp,
} from '../types/ot';
import { NodeInterface } from '../types/node';
import { getDocument, getWindow } from '../utils';
import { getDocument } from '../utils';
import { isCursorOp, isTransientElement, updateIndex, toDOM } from './utils';
import { $ } from '../node';
import { DATA_ID, EDITABLE_SELECTOR } from '../constants';
@ -51,7 +51,7 @@ class Consumer implements ConsumerInterface {
1 === path.length ||
pathOffset === JSON0_INDEX.TAG_NAME ||
pathOffset === JSON0_INDEX.ATTRIBUTE ||
childNode.nodeType === getWindow().Node.TEXT_NODE
childNode.nodeType === Node.TEXT_NODE
) {
return {
startNode: childNode,

View File

@ -19,13 +19,7 @@ import {
UI,
UI_SELECTOR,
} from '../constants/root';
import {
getParentInRoot,
getWindow,
toHex,
unescapeDots,
unescape,
} from '../utils';
import { getParentInRoot, toHex, unescapeDots, unescape } from '../utils';
export const isTransientElement = (
node: NodeInterface,
@ -343,7 +337,7 @@ export const toJSON0 = (
let values: Array<{} | string>;
if (!isTransientElement(node)) {
const { attributes, nodeValue } = node.get<Element>()!;
if (node.type === getWindow().Node.ELEMENT_NODE) {
if (node.type === Node.ELEMENT_NODE) {
values = [node.name];
const data = {};
for (let i = 0; attributes && i < attributes.length; i++) {
@ -362,9 +356,7 @@ export const toJSON0 = (
childToJSON0(node, values);
return values;
}
return node.type === getWindow().Node.TEXT_NODE
? String(nodeValue)
: undefined;
return node.type === Node.TEXT_NODE ? String(nodeValue) : undefined;
}
return;
};

View File

@ -17,7 +17,6 @@ import {
toHex,
transformCustomTags,
getListStyle,
getWindow,
} from '../utils';
import TextParser from './text';
import { $ } from '../node';
@ -57,7 +56,7 @@ const stylesToString = (styles: { [k: string]: string }) => {
};
class Parser implements ParserInterface {
private root: NodeInterface;
root: NodeInterface;
private editor: EditorInterface;
constructor(
source: string | Node | NodeInterface,
@ -79,10 +78,7 @@ class Parser implements ParserInterface {
.replace(/<p(>|\s+[^>]*>)/gi, '<paragraph$1')
.replace(/<\/p>/gi, '</paragraph>');
source = transformCustomTags(source);
const doc = new (getWindow().DOMParser)().parseFromString(
source,
'text/html',
);
const doc = new DOMParser().parseFromString(source, 'text/html');
this.root = $(doc.body);
const p = $('<p></p>');
const paragraphs = this.root.find('paragraph');
@ -94,7 +90,7 @@ class Parser implements ParserInterface {
Object.keys(attributes).forEach((name) => {
pNode.attributes(name, attributes[name]);
});
node.replace(cNode, pNode);
node.replace(cNode, pNode, true);
});
} else if (isNodeEntry(source)) {
this.root = source;
@ -126,7 +122,7 @@ class Parser implements ParserInterface {
const { rule } = value;
oldRules.push(rule);
const { name, attributes, style } = value.node;
delete attributes['data-id'];
delete attributes[DATA_ID];
const newNode = $(`<${name} />`);
nodeApi.setAttributes(newNode, {
...attributes,
@ -198,7 +194,7 @@ class Parser implements ParserInterface {
return newNode;
};
//当前节点是 inline 节点inline 节点不允许嵌套、不允许放入mark节点
inlineApi.flat(node);
inlineApi.flat(node, schema);
//当前节点是 mark 节点
if (nodeApi.isMark(node, schema)) {
//过滤掉当前mark节点属性样式并使用剩下的属性样式组成新的节点
@ -480,7 +476,7 @@ class Parser implements ParserInterface {
const node = domNode.get<HTMLElement>();
if (
node &&
node.nodeType === getWindow().Node.ELEMENT_NODE &&
node.nodeType === Node.ELEMENT_NODE &&
'none' === node.style['user-select'] &&
node.parentNode
) {

View File

@ -1,6 +1,6 @@
import { NodeInterface } from './types/node';
import { RangeInterface, RangePath } from './types/range';
import { getWindow, isMobile } from './utils';
import { isMobile } from './utils';
import { CARD_ELEMENT_KEY, CARD_SELECTOR } from './constants/card';
import { ANCHOR, CURSOR, FOCUS } from './constants/selection';
import {
@ -205,7 +205,7 @@ class Range implements RangeInterface {
*/
enlargeFromTextNode = () => {
const enlargePosition = (node: Node, offset: number, type: string) => {
if (node.nodeType !== getWindow().Node.TEXT_NODE) {
if (node.nodeType !== Node.TEXT_NODE) {
return;
}
if (offset === 0) {
@ -239,7 +239,7 @@ class Range implements RangeInterface {
*/
shrinkToTextNode = () => {
const shrinkPosition = (node: Node, offset: number, type: string) => {
if (node.nodeType !== getWindow().Node.ELEMENT_NODE) {
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
@ -260,12 +260,12 @@ class Range implements RangeInterface {
right = childNodes[offset];
}
if (left && left.nodeType === getWindow().Node.TEXT_NODE) {
if (left && left.nodeType === Node.TEXT_NODE) {
child = left;
offset = child.nodeValue?.length || 0;
}
if (right && right.nodeType === getWindow().Node.TEXT_NODE) {
if (right && right.nodeType === Node.TEXT_NODE) {
child = right;
offset = 0;
}
@ -305,7 +305,7 @@ class Range implements RangeInterface {
) => {
let domNode = $(node);
if (
domNode.type === getWindow().Node.TEXT_NODE ||
domNode.type === Node.TEXT_NODE ||
(!toBlock && nodeApi.isBlock(domNode)) ||
domNode.isEditable()
) {
@ -363,10 +363,10 @@ class Range implements RangeInterface {
let child;
let childDom;
while (
this.startContainer.nodeType === getWindow().Node.ELEMENT_NODE &&
this.startContainer.nodeType === Node.ELEMENT_NODE &&
(child = this.startContainer.childNodes[this.startOffset]) &&
(childDom = $(child)) &&
child.nodeType === getWindow().Node.ELEMENT_NODE &&
child.nodeType === Node.ELEMENT_NODE &&
!childDom.isCursor() &&
!node.isVoid(child) &&
(!childDom.isCard() ||
@ -376,11 +376,11 @@ class Range implements RangeInterface {
this.setStart(child, 0);
}
while (
this.endContainer.nodeType === getWindow().Node.ELEMENT_NODE &&
this.endContainer.nodeType === Node.ELEMENT_NODE &&
this.endOffset > 0 &&
(child = this.endContainer.childNodes[this.endOffset - 1]) &&
(childDom = $(child)) &&
child.nodeType === getWindow().Node.ELEMENT_NODE &&
child.nodeType === Node.ELEMENT_NODE &&
!node.isVoid(child) &&
!childDom.isCursor() &&
(!childDom.isCard() ||
@ -514,7 +514,7 @@ class Range implements RangeInterface {
if (
startContainer !== endContainer ||
collapsed === true ||
startContainer.nodeType === getWindow().Node.TEXT_NODE
startContainer.nodeType === Node.TEXT_NODE
) {
return elements;
}
@ -533,7 +533,7 @@ class Range implements RangeInterface {
getStartOffsetNode = (): Node => {
const { startContainer, startOffset } = this;
if (startContainer.nodeType === getWindow().Node.ELEMENT_NODE) {
if (startContainer.nodeType === Node.ELEMENT_NODE) {
return (
startContainer.childNodes[startOffset] ||
startContainer.childNodes[startOffset - 1] ||
@ -545,7 +545,7 @@ class Range implements RangeInterface {
getEndOffsetNode = (): Node => {
const { endContainer, endOffset } = this;
if (endContainer.nodeType === getWindow().Node.ELEMENT_NODE) {
if (endContainer.nodeType === Node.ELEMENT_NODE) {
return (
endContainer.childNodes[endOffset] ||
endContainer.childNodes[endOffset - 1] ||
@ -568,10 +568,7 @@ class Range implements RangeInterface {
scrollRangeIntoView = () => {
const node = this.getEndOffsetNode();
const root =
node.nodeType === getWindow().Node.TEXT_NODE
? node.parentNode
: node;
const root = node.nodeType === Node.TEXT_NODE ? node.parentNode : node;
const rect = this.collapsed
? (root as Element).getBoundingClientRect()
: this.getClientRect();
@ -890,13 +887,13 @@ Range.fromPath = (
offset = 0;
}
if (
node.nodeType === getWindow().Node.ELEMENT_NODE &&
node.nodeType === Node.ELEMENT_NODE &&
offset > node.childNodes.length
) {
offset = node.childNodes.length;
}
if (
node.nodeType === getWindow().Node.TEXT_NODE &&
node.nodeType === Node.TEXT_NODE &&
offset > (node.nodeValue?.length || 0)
) {
offset = node.nodeValue?.length || 0;

View File

@ -1,5 +1,5 @@
import { startsWith } from 'lodash-es';
import { getDocument, getWindow } from '../../utils';
import { getDocument } from '../../utils';
import { AjaxInterface, AjaxOptions, SetupOptions } from '../../types/request';
import { isFormData, toQueryString, urlAppend } from './utils';
import {
@ -53,7 +53,7 @@ class Ajax implements AjaxInterface {
...globalSetup,
...options,
url,
context: options.context || getWindow(),
context: options.context || window,
doc: options.doc || getDocument(),
jsonpCallback: options.jsonpCallback || 'callback',
method: options.method || 'GET',

View File

@ -10,7 +10,7 @@ import {
SchemaValue,
SchemaValueObject,
} from './types';
import { getWindow, validUrl } from './utils';
import { validUrl } from './utils';
import { getHashId } from './node';
import { DATA_ID } from './constants';
@ -149,23 +149,20 @@ class Schema implements SchemaInterface {
* @param callback
*/
find(callback: (rule: SchemaRule) => boolean): Array<SchemaRule> {
let schemas: Array<SchemaRule> = [];
Object.keys(this.data).some((key) => {
const schemas: Array<SchemaRule> = [];
Object.keys(this.data).forEach((key) => {
if (key !== 'globals') {
const rules = (this.data[key] as Array<SchemaRule>).filter(
(rule) => callback(rule),
callback,
);
if (rules && rules.length > 0) {
schemas = schemas.concat(rules);
}
schemas.push(...rules);
}
return;
});
return schemas;
}
getType(node: NodeInterface, filter?: (rule: SchemaRule) => boolean) {
if (node.type !== getWindow().Node.ELEMENT_NODE) return undefined;
if (node.type !== Node.ELEMENT_NODE) return undefined;
let id = node.attributes(DATA_ID);
if (!id) id = getHashId(node, false);
else id = id.split('-')[0];
@ -184,7 +181,7 @@ class Schema implements SchemaInterface {
*/
getRule(node: NodeInterface, filter?: (rule: SchemaRule) => boolean) {
filter = filter || ((rule) => rule.name === node.name);
if (node.type !== getWindow().Node.ELEMENT_NODE) return undefined;
if (node.type !== Node.ELEMENT_NODE) return undefined;
return this._all.find(
(rule) => filter!(rule) && this.checkNode(node, rule.attributes),
);
@ -197,16 +194,15 @@ class Schema implements SchemaInterface {
*/
checkNode(
node: NodeInterface,
attributes?: SchemaAttributes | SchemaStyle,
attributes: SchemaAttributes | SchemaStyle = {},
): boolean {
//获取节点属性
const nodeAttributes = node.attributes();
const nodeStyles = node.css();
delete nodeAttributes['style'];
const styles = (attributes || {}).style as SchemaAttributes;
attributes = omit({ ...attributes }, 'style');
const styles = (attributes.style || {}) as SchemaAttributes;
//需要属性每一项都能效验通过
const attrResult = Object.keys(attributes).every((attributesName) => {
if (attributesName === 'style') return true;
return this.checkValue(
attributes as SchemaAttributes,
attributesName,
@ -214,7 +210,7 @@ class Schema implements SchemaInterface {
);
});
if (!attrResult) return false;
return Object.keys(styles || {}).every((styleName) => {
return Object.keys(styles).every((styleName) => {
return this.checkValue(styles, styleName, nodeStyles[styleName]);
});
}
@ -324,15 +320,14 @@ class Schema implements SchemaInterface {
* @param rule
*/
filterStyles(styles: { [k: string]: string }, rule: SchemaRule) {
if (!rule.attributes?.style) {
styles = {};
return;
}
Object.keys(styles).forEach((styleName) => {
//没有设置规则,全部删除
if (!rule.attributes?.style) {
delete styles[styleName];
return;
}
if (
!this.checkValue(
rule?.attributes?.style! as SchemaAttributes,
rule.attributes!.style as SchemaAttributes,
styleName,
styles[styleName],
true,
@ -347,15 +342,14 @@ class Schema implements SchemaInterface {
* @param rule
*/
filterAttributes(attributes: { [k: string]: string }, rule: SchemaRule) {
if (!rule.attributes) {
attributes = {};
return;
}
Object.keys(attributes).forEach((attributesName) => {
//没有设置规则,全部删除
if (!rule.attributes) {
delete attributes[attributesName];
return;
}
if (
!this.checkValue(
rule?.attributes! as SchemaAttributes,
rule.attributes as SchemaAttributes,
attributesName,
attributes[attributesName],
true,
@ -379,18 +373,18 @@ class Schema implements SchemaInterface {
) {
const rule = this.getRule(node);
if (!rule) return;
let allRule: SchemaRule = { ...rule };
const globalRule = Object.keys(this.data.globals).find(
const { globals } = this.data;
const globalRule = Object.keys(globals).find(
(dataType) => rule.type === dataType,
);
if (globalRule) {
allRule.attributes = merge(
{
...allRule.attributes,
},
{ ...this.data.globals[globalRule] },
);
}
const allRule = {
...omit(rule, 'attributes'),
attributes: merge(
{},
rule.attributes,
globalRule ? globals[globalRule] : {},
),
};
this.filterAttributes(attributes, allRule);
this.filterStyles(styles, allRule);
}
@ -403,19 +397,18 @@ class Schema implements SchemaInterface {
*/
closest(name: string) {
let topName = name;
this.data.blocks
.filter((rule) => rule.name === name)
.forEach((block) => {
const schema = block as SchemaBlock;
if (schema.allowIn) {
schema.allowIn.forEach((parentName) => {
if (this.isAllowIn(parentName, topName)) {
topName = parentName;
}
});
topName = this.closest(topName);
}
});
this.data.blocks.forEach((block) => {
if (block.name !== name) return;
const schema = block as SchemaBlock;
if (schema.allowIn) {
schema.allowIn.forEach((parentName) => {
if (this.isAllowIn(parentName, topName)) {
topName = parentName;
}
});
topName = this.closest(topName);
}
});
return topName;
}
/**
@ -427,15 +420,14 @@ class Schema implements SchemaInterface {
isAllowIn(source: string, target: string) {
//p节点下不允许放其它block节点
if (source === 'p') return false;
return this.data.blocks
.filter((rule) => rule.name === target)
.some((block) => {
const schema = block as SchemaBlock;
if (schema.allowIn && schema.allowIn.indexOf(source) > -1) {
return true;
}
return;
});
return this.data.blocks.some((block) => {
if (block.name !== target) return;
const schema = block as SchemaBlock;
if (schema.allowIn && schema.allowIn.indexOf(source) > -1) {
return true;
}
return;
});
}
addAllowIn(parent: string, child: string = 'p') {
const rule = this.data.blocks.find(

View File

@ -23,7 +23,7 @@ import { BlockModelInterface } from './block';
import { RequestInterface } from './request';
import { RangeInterface } from './range';
import { Op } from 'sharedb';
import { NodeIdInterface } from 'src';
import { NodeIdInterface } from './';
/**
*
@ -727,7 +727,7 @@ export interface EngineInterface extends EditorInterface {
): void;
/**
*
* @param node
* @param node
*/
on(
eventType: 'paste:each-after',
@ -896,7 +896,7 @@ export interface EngineInterface extends EditorInterface {
off(eventType: 'paste:each', listener: (root: NodeInterface) => void): void;
/**
*
* @param node
* @param node
*/
off(
eventType: 'paste:each-after',
@ -1023,7 +1023,7 @@ export interface EngineInterface extends EditorInterface {
trigger(eventType: 'paste:each', root: NodeInterface): void;
/**
*
* @param node
* @param node
*/
trigger(eventType: 'paste:each-after', root: NodeInterface): void;
/**

View File

@ -1,6 +1,7 @@
import { NodeInterface } from './node';
import { PluginInterface, ElementPluginInterface } from './plugin';
import { RangeInterface } from './range';
import { SchemaInterface } from './schema';
export interface InlineModelInterface {
/**
@ -56,7 +57,7 @@ export interface InlineModelInterface {
* inline节点mark标签内inline标签
* @param node
*/
flat(node: NodeInterface | RangeInterface): void;
flat(node: NodeInterface | RangeInterface, schema?: SchemaInterface): void;
}
export interface InlineInterface extends ElementPluginInterface {

View File

@ -623,8 +623,13 @@ export interface NodeModelInterface {
*
* @param source
* @param target
* @param copyId id
*/
replace(source: NodeInterface, target: NodeInterface): NodeInterface;
replace(
source: NodeInterface,
target: NodeInterface,
copyId?: boolean,
): NodeInterface;
/**
*
* @param node

View File

@ -28,6 +28,21 @@ export type Callbacks = {
};
export interface ParserInterface {
/**
*
*/
root: NodeInterface;
/**
*
* @param root
* @param schema Schema
* @param conversion
*/
normalize(
root: NodeInterface,
schema: SchemaInterface,
conversion: ConversionInterface | null,
): void;
/**
*
* @param node

View File

@ -4,7 +4,6 @@ import {
RangeInterface,
TypingHandleInterface,
} from '../../types';
import { getWindow } from '../../utils';
import { CARD_KEY } from '../../constants';
import Range from '../../range';
import { $ } from '../../node';
@ -72,7 +71,7 @@ class Delete implements TypingHandleInterface {
range.collapse(true);
return range;
}
if (node.nodeType === getWindow().Node.TEXT_NODE) {
if (node.nodeType === Node.TEXT_NODE) {
if (node['data'].length === 0) return this.getRange(node);
if (!node.ownerDocument) return null;
const range = Range.create(this.engine, node.ownerDocument);
@ -107,7 +106,7 @@ class Delete implements TypingHandleInterface {
}
if (!card.isRightCursor(range.startNode)) return;
nextNode = card.root[0];
} else if (range.endContainer.nodeType === getWindow().Node.TEXT_NODE) {
} else if (range.endContainer.nodeType === Node.TEXT_NODE) {
if (range.endContainer['data'].length > range.endOffset) {
event.preventDefault();
const cloneRange = range.cloneRange();
@ -119,8 +118,7 @@ class Delete implements TypingHandleInterface {
}
nextNode = range.endContainer;
} else {
if (range.endContainer.nodeType !== getWindow().Node.ELEMENT_NODE)
return;
if (range.endContainer.nodeType !== Node.ELEMENT_NODE) return;
if (range.endContainer.childNodes.length === 0) {
nextNode = range.endContainer;
} else if (range.endOffset === 0) {

View File

@ -16,18 +16,6 @@ export const getDocument = (node?: Node): Document => {
: document;
};
export const getWindow = (node?: Node): Window & typeof globalThis => {
if (
typeof window === 'undefined' &&
typeof global['__amWindow'] === 'undefined'
)
throw 'window is not defined,If you are using ssr, you can assign a value to the `__amWindow` global variable.';
const win = typeof window === 'undefined' ? global['__amWindow'] : window;
if (!node) return win;
const document = getDocument(node);
return document['parentWindow'] || document.defaultView || win;
};
/**
*
* @param node

View File

@ -5,8 +5,6 @@ import {
READY_CARD_KEY,
} from '../constants/card';
import { DATA_ELEMENT } from '../constants/root';
import { getWindow } from './node';
import { isMacos } from './user-agent';
/**
@ -44,7 +42,7 @@ export const toCamelCase = (
return value
.split('-')
.map((str, index) => {
if (type === 'upper' || (type === 'lower' && index > 0)) {
if (type === 'upper' || index > 0) {
return str.charAt(0).toUpperCase() + str.substr(1);
}
if (type === 'lower' && index === 0) {
@ -96,6 +94,7 @@ export const getAttrMap = (value: string): { [k: string]: string } => {
return map;
};
const stylesCaches = new Map<string, { [k: string]: string }>();
/**
* style map
* @param {string} style
@ -103,15 +102,21 @@ export const getAttrMap = (value: string): { [k: string]: string } => {
export const getStyleMap = (style: string): { [k: string]: string } => {
style = style.replace(/&quot;/g, '"');
const map: { [k: string]: string } = {};
if (!style) return map;
const cacheStyle = stylesCaches.get(style);
if (cacheStyle) return cacheStyle;
const reg = /\s*([\w\-]+)\s*:([^;]*)(;|$)/g;
let match;
while ((match = reg.exec(style))) {
const key = match[1].toLowerCase().trim();
const val = toHex(match[2]).trim();
let val = match[2].trim();
if (val.toLowerCase().includes('rgb')) {
val = toHex(match[2]);
}
map[key] = val;
}
stylesCaches.set(style, map);
return map;
};
@ -121,9 +126,8 @@ export const getStyleMap = (style: string): { [k: string]: string } => {
* @param {string} attrName
*/
export const getComputedStyle = (element: Element, attrName: string) => {
const win = getWindow(element);
const camelKey = toCamelCase(attrName);
const style = win?.getComputedStyle(element, null);
const style = window.getComputedStyle(element, null);
return style ? style[camelKey] : '';
};

View File

@ -1,7 +1,5 @@
import { getWindow } from './node';
const userAgent = (
typeof navigator !== 'undefined' ? navigator : getWindow().navigator
typeof navigator !== 'undefined' ? navigator : window.navigator
).userAgent.toLowerCase();
export const isServer = typeof navigator === 'undefined';
/**

View File

@ -27,7 +27,7 @@ export const autoGetHotkey = (
}
return;
};
const supportFontFamilyCache: { [key: string]: boolean } = {};
/**
*
* @param font
@ -38,7 +38,8 @@ export const isSupportFontFamily = (font: string) => {
console.log('Font name is not legal !');
return false;
}
if (supportFontFamilyCache[font] !== undefined)
return supportFontFamilyCache[font];
let width;
const body = document.body;
@ -64,9 +65,10 @@ export const isSupportFontFamily = (font: string) => {
const serifWidth = getWidth('serif');
const sansWidth = getWidth('sans-serif');
return (
const reuslt =
monoWidth !== getWidth(font + ',monospace') ||
sansWidth !== getWidth(font + ',sans-serif') ||
serifWidth !== getWidth(font + ',serif')
);
serifWidth !== getWidth(font + ',serif');
supportFontFamilyCache[font] = reuslt;
return reuslt;
};

View File

@ -27,7 +27,7 @@ export const autoGetHotkey = (
}
return;
};
const supportFontFamilyCache: { [key: string]: boolean } = {};
/**
*
* @param font
@ -38,7 +38,8 @@ export const isSupportFontFamily = (font: string) => {
console.log('Font name is not legal !');
return false;
}
if (supportFontFamilyCache[font] !== undefined)
return supportFontFamilyCache[font];
let width;
const body = document.body;
@ -64,9 +65,10 @@ export const isSupportFontFamily = (font: string) => {
const serifWidth = getWidth('serif');
const sansWidth = getWidth('sans-serif');
return (
const reuslt =
monoWidth !== getWidth(font + ',monospace') ||
sansWidth !== getWidth(font + ',sans-serif') ||
serifWidth !== getWidth(font + ',serif')
);
serifWidth !== getWidth(font + ',serif');
supportFontFamilyCache[font] = reuslt;
return reuslt;
};

View File

@ -75,7 +75,8 @@ export default class extends MarkPlugin<Options> {
pasteEach(node: NodeInterface) {
//pt 转为px
if (node.name === this.tagName) {
const fontFamily = node.css(this.#styleName);
const styles = node.css();
const fontFamily = styles[this.#styleName];
if (!fontFamily) return;
const { filter } = this.options;
if (filter) {

View File

@ -2,7 +2,6 @@ import { PswpInterface } from '@/types';
import {
$,
EditorInterface,
getWindow,
isEngine,
escape,
NodeInterface,
@ -83,7 +82,7 @@ export type Options = {
onError?: () => void;
};
export const winPixelRatio = getWindow().devicePixelRatio;
export const winPixelRatio = window.devicePixelRatio;
let pswp: PswpInterface | undefined = undefined;
class Image {
private editor: EditorInterface;

View File

@ -4,7 +4,6 @@ import PhotoSwipeUI from 'photoswipe/dist/photoswipe-ui-default';
import {
$,
EditorInterface,
getWindow,
isHotkey,
isMobile,
NodeInterface,
@ -125,7 +124,7 @@ class Pswp extends EventEmitter2 implements PswpInterface {
bindClickEvent() {
const onClick = (event: MouseEvent | TouchEvent) => {
const node =
getWindow().TouchEvent && event instanceof TouchEvent
window.TouchEvent && event instanceof TouchEvent
? $(event.touches[0].target)
: $(event.target || []);
if (node.hasClass('pswp__img')) {

View File

@ -1,10 +1,4 @@
import {
$,
NodeInterface,
EventListener,
isMobile,
getWindow,
} from '@aomao/engine';
import { $, NodeInterface, EventListener, isMobile } from '@aomao/engine';
import './index.css';
export type Options = {
@ -93,11 +87,11 @@ class Resizer {
);
this.point = {
x:
getWindow().TouchEvent && event instanceof TouchEvent
TouchEvent && event instanceof TouchEvent
? event.touches[0].clientX
: (event as MouseEvent).clientX,
y:
getWindow().TouchEvent && event instanceof TouchEvent
TouchEvent && event instanceof TouchEvent
? event.touches[0].clientY
: (event as MouseEvent).clientY,
};
@ -122,7 +116,7 @@ class Resizer {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY } =
getWindow().TouchEvent && event instanceof TouchEvent
TouchEvent && event instanceof TouchEvent
? event.touches[0]
: (event as MouseEvent);

View File

@ -282,8 +282,13 @@ export default class extends Plugin<Options> {
pasteEach(node: NodeInterface) {
//pt 转为px
if (!node.isCard() && this.editor.node.isBlock(node)) {
const textIndentSource = node.css(TEXT_INENT_KEY);
if (
node.isElement() &&
!node.isCard() &&
this.editor.node.isBlock(node)
) {
const styles = node.css();
const textIndentSource = styles[TEXT_INENT_KEY];
if (!!textIndentSource && textIndentSource.endsWith('pt')) {
const textIndent = this.convertToPX(textIndentSource);
if (!!textIndent) {

View File

@ -90,14 +90,11 @@ export default class extends InlinePlugin<Options> {
query() {
if (!isEngine(this.editor)) return;
const { change, inline } = this.editor;
const range = change.range.get();
const inlineNode = inline
.findInlines(range)
.find((node) => this.isSelf(node));
const { change } = this.editor;
const inlineNode = change.inlines.find((node) => this.isSelf(node));
this.toolbar?.hide(inlineNode);
if (inlineNode && !inlineNode.isCard()) {
if (range.collapsed) this.toolbar?.show(inlineNode);
this.toolbar?.show(inlineNode);
return true;
}
return false;

View File

@ -94,14 +94,11 @@ export default class extends InlinePlugin<Options> {
query() {
if (!isEngine(this.editor)) return;
const { change, inline } = this.editor;
const range = change.range.get();
const inlineNode = inline
.findInlines(range)
.find((node) => this.isSelf(node));
const { change } = this.editor;
const inlineNode = change.inlines.find((node) => this.isSelf(node));
this.toolbar?.hide(inlineNode);
if (inlineNode && !inlineNode.isCard()) {
if (range.collapsed) this.toolbar?.show(inlineNode);
if (inlineNode && inlineNode.length > 0 && !inlineNode.isCard()) {
this.toolbar?.show(inlineNode);
return true;
}
return false;

View File

@ -572,7 +572,7 @@ class TableCommand extends EventEmitter2 implements TableCommandInterface {
conversion,
);
const element = helper.trimBlankSpan($(pasteHTML));
this.editor.nodeId.generateAll(element, true);
if (element.name === 'table') {
helper.normalizeTable(element);
const pasteTableModel = helper.getTableModel(element);

View File

@ -363,9 +363,14 @@ class TableComponent extends Card<TableValue> implements TableInterface {
}
});
this.scrollbar.disableScroll();
this.scrollbar.on('change', () => {
if (isEngine(this.editor)) this.editor.ot.initSelection();
});
let changeTimeout: NodeJS.Timeout | undefined;
const handleScrollbarChange = () => {
if (changeTimeout) clearTimeout(changeTimeout);
changeTimeout = setTimeout(() => {
if (isEngine(this.editor)) this.editor.ot.initSelection();
}, 50);
};
this.scrollbar.on('change', handleScrollbarChange);
}
this.scrollbar?.refresh();
this.selection.on('select', () => {

View File

@ -122,7 +122,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
width: 100%;
cursor: default;
margin-bottom: -1px;
z-index: 1;
z-index: 2;
}
.table-wrapper.active .table-cols-header {

View File

@ -31,7 +31,7 @@ class Table extends Plugin<Options> {
editor.schema.add(this.schema());
editor.conversion.add('th', 'td');
editor.on('parse:html', (node) => this.parseHtml(node));
editor.on('paste:each-after', (child) => this.pasteHtml(child));
editor.on('paste:each-after', (root) => this.pasteHtml(root));
editor.on('paste:schema', (schema: SchemaInterface) =>
this.pasteSchema(schema),
);
@ -266,7 +266,7 @@ class Table extends Plugin<Options> {
return value;
}
pasteHtml(node: NodeInterface) {
pasteHtml(root: NodeInterface) {
if (!isEngine(this.editor)) return;
const clearWH = (
node: NodeInterface,
@ -277,7 +277,10 @@ class Table extends Plugin<Options> {
if (width.endsWith('%')) node.css(type, '');
if (width.endsWith('pt')) node.css(type, this.convertToPX(width));
};
if (node.name === 'table') {
const tables = root.find('table');
tables.each((_, index) => {
const node = tables.eq(index);
if (!node) return;
clearWH(node);
clearWH(node, 'height');
// 表头放在tbody最前面
@ -339,9 +342,7 @@ class Table extends Plugin<Options> {
.outerHTML.replace(/\n|\r\n/g, '')
.replace(/>\s+</g, '><'),
});
return false;
}
return true;
});
}
parseHtml(root: NodeInterface) {

View File

@ -49,11 +49,13 @@ export default class extends ListPlugin<Options> {
this.editor.on('paste:markdown', (child) =>
this.pasteMarkdown(child),
);
this.editor.on('paste:each-after', (child) => {
if (
child.name === 'li' &&
child.hasClass(this.editor.list.CUSTOMZIE_LI_CLASS)
) {
this.editor.on('paste:each-after', (root) => {
const liNodes = root.find(
`li.${this.editor.list.CUSTOMZIE_LI_CLASS}`,
);
liNodes.each((_, index) => {
const child = liNodes.eq(index);
if (!child) return;
const firstChild = child.first();
if (
firstChild &&
@ -71,7 +73,7 @@ export default class extends ListPlugin<Options> {
}
}
}
}
});
});
}
}