fix: 协同与异步卡片加载会造成写入多个无意义节点

This commit is contained in:
yanmao 2021-12-17 13:13:13 +08:00
parent d9ccd8faa5
commit ba73863f7c
32 changed files with 352 additions and 192 deletions

View File

@ -124,11 +124,14 @@ Download address: response.download || response.data && response.data.download T
/**
* Parse the uploaded Respone and return result: whether it is successful or not, data: success: file address, failure: error message
*/
parse?: (
response: any,
) => {
parse?: (response: any) => {
result: boolean;
data: string;
data: {
url: string;
preview?: string;
download?: string;
status?: string;
} | string;
};
```

View File

@ -124,11 +124,14 @@ limitSize?: number;
/**
* 解析上传后的Respone返回 result:是否成功data:成功:文件地址,失败:错误信息
*/
parse?: (
response: any,
) => {
parse?: (response: any) => {
result: boolean;
data: string;
data: {
url: string;
preview?: string;
download?: string;
status?: string;
} | string;
};
```

View File

@ -32,7 +32,7 @@ class Button {
const rootRect = root.get<HTMLElement>()!.getBoundingClientRect();
const top = rangeRect.y - rootRect.y;
this.#container.css('top', `${top}px`);
this.#container.css('right', `16px`);
this.#container.css('right', `-16px`);
this.#container.show('flex');
}

View File

@ -125,7 +125,7 @@
}
.editor-content .am-engine {
padding: 0 0 60px;
padding: 40px 0 60px;
@media @mobile {
padding: 0;

View File

@ -333,9 +333,6 @@ const EditorComponent: React.FC<EditorProps> = ({
<OTComponent members={members} />,
headerOTMembersElement,
);
return () => {
ReactDOM.unmountComponentAtNode(headerOTMembersElement);
};
}, [members, props.ot]);
return (

View File

@ -1,7 +1,7 @@
.data-toc-wrapper {
position: absolute;
top: 20px;
min-width: 210px;
max-width: 210px;
padding: 0 16px;
}

View File

@ -361,9 +361,6 @@ abstract class CardEntry<T extends CardValue = {}> implements CardInterface {
}
didRender() {
if (this.loading) this.find(`.${CARD_LOADING_KEY}`).remove();
setTimeout(() => {
this.root.removeAttributes(CARD_LOADING_KEY);
}, 100);
if (this.resize) {
const container =
typeof this.resize === 'function'

View File

@ -660,6 +660,7 @@ class CardModel implements CardModelInterface {
this.removeComponent(card);
}
cardNode.attributes(CARD_LOADING_KEY, 'true');
attributes[CARD_LOADING_KEY] = 'true';
cardNode.empty();
}
//ready_card_key 待创建的需要重新生成节点,并替换当前待创建节点

View File

@ -441,7 +441,6 @@ class ChangeModel implements ChangeInterface {
let node: NodeInterface | null = $(childNodes[0]);
let prev: NodeInterface | null = null;
const appendNodes = [];
let startRangeNodeParent = startRange.node.parent();
while (node && node.length > 0) {
nodeApi.removeSide(node);
const next: NodeInterface | null = node.next();
@ -460,12 +459,8 @@ class ChangeModel implements ChangeInterface {
range.select(node, true).collapse(false);
}
// 被删除了重新设置开始节点位置
if (
startRange &&
(!startRangeNodeParent || startRangeNodeParent.length === 0)
) {
if (startRange && !startRange.node[0].parentNode) {
const children = node.children();
startRangeNodeParent = node.parent();
startRange = {
node: node,
offset:

View File

@ -537,7 +537,9 @@ class NativeEvent {
) => void,
) {
const { change } = this.engine;
const fragment = new Paste(source, this.engine).normalize();
const fragment = new Paste(source, this.engine).normalize(
insert === undefined,
);
this.engine.trigger('paste:before', fragment);
if (insert) insert(fragment, range, undefined, followActiveMark);
else

View File

@ -297,7 +297,7 @@ export default class Paste {
});
}
normalize() {
normalize(autoAppendCurrent: boolean = true) {
const nodeApi = this.engine.node;
let fragment = this.parser();
this.elementNormalize(fragment);
@ -376,6 +376,7 @@ export default class Paste {
.shrinkToTextNode();
const { startNode } = cloneRange;
if (
autoAppendCurrent &&
startNode.inEditor() &&
first &&
first.name === 'p' &&

View File

@ -332,14 +332,15 @@ class Engine implements EngineInterface {
setHtml(html: string, callback?: (count: number) => void) {
this.change.setHtml(html, (count) => {
this.normalize();
this.container.allChildren(true).forEach((child) => {
if (this.node.isInline(child)) {
this.inline.repairCursor(child);
} else if (this.node.isMark(child)) {
this.mark.repairCursor(child);
}
if (callback) callback(count);
});
if (callback) callback(count);
});
this.nodeId.generateAll(this.container);
return this;

View File

@ -106,7 +106,7 @@ class HistoryModel implements HistoryInterface {
console.error(error);
}
if (this.engine.isEmpty()) this.engine.change.initValue();
this.engine.ot.startMutation();
if (isUndo) {
//清除操作前记录的range
this.engine.change.getRangePathBeforeCommand();
@ -114,6 +114,7 @@ class HistoryModel implements HistoryInterface {
this.engine.change.change();
this.engine.trigger('undo');
}
this.engine.ot.startMutation();
}
}
@ -139,7 +140,7 @@ class HistoryModel implements HistoryInterface {
this.reset();
console.error(error);
}
this.engine.ot.startMutation();
if (isRedo) {
// 清除操作前记录的range
this.engine.change.getRangePathBeforeCommand();
@ -147,6 +148,7 @@ class HistoryModel implements HistoryInterface {
this.engine.change.change();
this.engine.trigger('redo');
}
this.engine.ot.startMutation();
}
}

View File

@ -42,6 +42,15 @@ class Consumer implements ConsumerInterface {
endOffset: index,
};
const offset = index - JSON0_INDEX.ELEMENT;
// 正在加载中的节点,直接渲染
if (
node.nodeType === Node.ELEMENT_NODE &&
(node as HTMLElement).hasAttribute(CARD_LOADING_KEY)
) {
const { card } = this.engine;
const cardComponent = card.find(node);
if (cardComponent) card.renderComponent(cardComponent);
}
const childNode = Array.from(node.childNodes).filter((node) => {
const childNode = $(node);
return !isTransientElement(childNode);
@ -51,7 +60,7 @@ class Consumer implements ConsumerInterface {
1 === path.length ||
pathOffset === JSON0_INDEX.TAG_NAME ||
pathOffset === JSON0_INDEX.ATTRIBUTE ||
childNode.nodeType === Node.TEXT_NODE
(childNode && childNode.nodeType === Node.TEXT_NODE)
) {
return {
startNode: childNode,

View File

@ -14,7 +14,7 @@ import {
updateIndex,
opsSort,
} from './utils';
import { escapeDots, escape } from '../utils/string';
import { escapeDots, escape, decodeCardValue } from '../utils/string';
import { toJSON0, getValue } from './utils';
import { EngineInterface } from '../types/engine';
import { Op, Path, StringInsertOp, StringDeleteOp, Doc } from 'sharedb';
@ -22,15 +22,20 @@ import { NodeInterface } from '../types/node';
import { DocInterface, RepairOp } from '../types/ot';
import { $ } from '../node';
import {
CARD_ELEMENT_KEY,
CARD_CENTER_SELECTOR,
CARD_KEY,
CARD_LOADING_KEY,
CARD_SELECTOR,
CARD_VALUE_KEY,
DATA_ELEMENT,
DATA_ID,
DATA_TRANSIENT_ELEMENT,
JSON0_INDEX,
UI,
UI_SELECTOR,
} from '../constants';
import { getDocument } from '../utils/node';
import { CardValue } from 'src';
import type { CardEntry } from '../types/card';
class Producer extends EventEmitter2 {
private engine: EngineInterface;
@ -519,18 +524,24 @@ class Producer extends EventEmitter2 {
const tMapValue = cardMap.get(cardElement[0]);
if (tMapValue === undefined && cardElement.isEditableCard()) {
const cardName = cardElement.attributes(CARD_KEY);
const cardValue = decodeCardValue(
cardElement.attributes(CARD_VALUE_KEY),
);
const result = this.findCardForDoc(
this.doc.data,
cardName,
(attriables) => {
return (
attriables[DATA_ID] ===
cardElement.attributes(DATA_ID)
// 卡片id一致
const value = decodeCardValue(
attriables[CARD_VALUE_KEY],
);
return value.id === cardValue.id;
},
);
// 没有这个卡片节点,或者卡片内部已经渲染了才需要过滤
if (
!result?.rendered &&
result &&
!result.rendered &&
cardElement.attributes(CARD_LOADING_KEY) !== 'remote'
) {
isTransient = false;
@ -542,6 +553,83 @@ class Producer extends EventEmitter2 {
} else if (tMapValue !== undefined) {
isTransient = tMapValue;
}
// 标记节点为已处理
const { addedNodes } = record;
addedNodes.forEach((addNode) => {
addNode['__card_rendered'] = true;
});
// 需要比对异步加载的卡片子节点(body -> center -> 非 ui 和 data-transient-element节点)是否已经处理完,处理完就移除掉卡片根节点的 CARD_LOADING_KEY 标记
// card.root.removeAttributes(CARD_LOADING_KEY);
// 判断卡片下面的节点
const isRendered = cardElement.isEditableCard()
? cardElement
.find(CARD_CENTER_SELECTOR)
.children()
.toArray()
.every((child) => {
if (child.length === 0) return true;
const attributes = child.attributes();
if (
attributes[DATA_ELEMENT] === UI ||
!!attributes[DATA_TRANSIENT_ELEMENT]
) {
return true;
}
if (child[0]['__card_rendered'] === true) {
return true;
}
return false;
})
: true;
if (isRendered) {
const handleEditableCard = (
editableCard: NodeInterface,
) => {
const childAllLoaded = editableCard
.find(CARD_SELECTOR)
.toArray()
.every((childCard) => {
const childLoading =
childCard.attributes(CARD_LOADING_KEY);
if (!childLoading) {
return true;
}
// 如果子卡片是懒加载的,则算已加载完成
const cardComponent =
this.engine.card.find(childCard);
if (
(cardComponent?.constructor as CardEntry)
.lazyRender
) {
return true;
}
return false;
});
if (childAllLoaded) {
editableCard.removeAttributes(CARD_LOADING_KEY);
}
};
// 可编辑卡片需要查看子卡片是否都渲染成功
if (cardElement.isEditableCard()) {
handleEditableCard(cardElement);
} else {
cardElement.removeAttributes(CARD_LOADING_KEY);
// 非可编辑卡片需要判断当前是否在可编辑器卡内,加载成功需要判断父级的可编辑卡片是否都加载成功
const cardParentElement = cardElement.parent();
if (cardParentElement) {
const parentEditableCard = this.engine.card.closest(
cardParentElement,
true,
);
if (
parentEditableCard &&
parentEditableCard.length > 0
) {
handleEditableCard(parentEditableCard);
}
}
}
}
}
if (
!isTransient &&
@ -564,7 +652,6 @@ class Producer extends EventEmitter2 {
return !isTransient;
});
this.clearCache();
//重置缓存
this.cacheTransientElements = undefined;
let ops = this.generateOps(records);

View File

@ -170,25 +170,8 @@ class Parser implements ParserInterface {
});
return;
}
root.traverse((node) => {
if (
node.equal(root) ||
['style', 'script', 'meta'].includes(node.name)
)
return;
if (node.isElement()) {
const isCard = node.isCard();
//转换标签
if (conversion && (!schema.getType(node) || isCard)) {
const newNode = this.convert(conversion, node, schema);
if (newNode) {
this.normalize(newNode, schema, conversion);
return;
}
}
if (isCard) return;
//分割
const filter = (node: NodeInterface) => {
// 过滤分割
const filter = (node: NodeInterface, type: string = 'mark') => {
//获取节点属性样式
const attributes = node.attributes();
const style = getStyleMap(attributes.style || '');
@ -225,12 +208,16 @@ class Parser implements ParserInterface {
if (
filterAttrCount === attrKeys.length &&
filterStyleCount === styleKeys.length &&
schema.getType(newNode) === 'mark'
schema.getType(newNode) === type
) {
node.before(newNode);
const children = node.children();
newNode.append(
children.length > 0 ? children : $('\u200b', null),
children.length > 0
? children
: type === 'block'
? '<br />'
: $('\u200b', null),
);
node.remove();
node = newNode;
@ -240,8 +227,23 @@ class Parser implements ParserInterface {
newNode.removeAttributes('style');
return newNode;
};
//当前节点是 inline 节点inline 节点不允许嵌套、不允许放入mark节点
inlineApi.flat(node, schema);
root.traverse((node) => {
if (
node.equal(root) ||
['style', 'script', 'meta'].includes(node.name)
)
return;
if (node.isElement()) {
const isCard = node.isCard();
//转换标签
if (conversion && (!schema.getType(node) || isCard)) {
const newNode = this.convert(conversion, node, schema);
if (newNode) {
this.normalize(newNode, schema, conversion);
return;
}
}
if (isCard) return;
//当前节点是 mark 节点
if (nodeApi.isMark(node, schema)) {
//过滤掉当前mark节点属性样式并使用剩下的属性样式组成新的节点
@ -285,6 +287,9 @@ class Parser implements ParserInterface {
oldRules.push(rule);
}
}
} else if (nodeApi.isInline(node)) {
//当前节点是 inline 节点inline 节点不允许嵌套、不允许放入mark节点
inlineApi.flat(node, schema);
}
}
});

View File

@ -913,15 +913,19 @@ Range.fromPath = (
? root.find(`[${DATA_ID}="${path.start.id}"]`)
: root;
const startNode = getNode(
path.start.bi > -1 ? startPath.slice(path.start.bi) : startPath,
beginContext,
path.start.bi > -1 && beginContext.length > 0
? startPath.slice(path.start.bi)
: startPath,
beginContext.length > 0 ? beginContext : undefined,
);
const endContext = path.end.id
? root.find(`[${DATA_ID}="${path.end.id}"]`)
: root;
const endNode = getNode(
path.end.bi > -1 ? endPath.slice(path.end.bi) : endPath,
endContext,
path.end.bi > -1 && endContext.length > 0
? endPath.slice(path.end.bi)
: endPath,
endContext.length > 0 ? endContext : undefined,
);
const range = Range.create(editor, document);
setRange(

View File

@ -320,12 +320,9 @@ 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 ||
!this.checkValue(
rule.attributes!.style as SchemaAttributes,
styleName,
@ -342,12 +339,9 @@ 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 ||
!this.checkValue(
rule.attributes as SchemaAttributes,
attributesName,

View File

@ -196,7 +196,7 @@ class Scrollbar extends EventEmitter2 {
left = getScrollLeft
? getScrollLeft(-0) + element.scrollLeft - left
: element.scrollLeft - left;
if (left < 0) left = 0;
if (onScrollX) {
const result = onScrollX(left);
if (result > 0) element.scrollLeft = result;

View File

@ -313,6 +313,7 @@ export interface CardInterface {
beforeRender?(): void;
/**
*
* @param args
*/
render(...args: any): NodeInterface | string | void;
/**
@ -540,6 +541,12 @@ export interface CardModelInterface {
callback?: (count: number) => void,
lazyRender?: boolean,
): void;
/**
*
* @param card
* @param args
*/
renderComponent(card: CardInterface, ...args: any): void;
/**
*
* @param cards

View File

@ -1,8 +1,10 @@
import { isMobile } from '../../utils';
import Default from './default';
class At extends Default {
hotkey = (event: KeyboardEvent) =>
event.key === '@' ||
(event.shiftKey && event.keyCode === 229 && event.code === 'Digit2');
(event.shiftKey && event.keyCode === 229 && event.code === 'Digit2') ||
(isMobile && event.keyCode === 229);
}
export default At;

View File

@ -23,6 +23,7 @@ class ToolbarComponent extends Card<{ data: Data }> {
private placeholder?: NodeInterface;
private component?: CollapseComponentInterface;
#collapseData?: Data;
#data?: any;
static get cardName() {
return 'toolbar';
@ -55,6 +56,10 @@ class ToolbarComponent extends Card<{ data: Data }> {
});
}
setData(_data: any) {
this.#data = _data;
}
getData(): Data {
if (!isEngine(this.editor)) {
return [];
@ -74,9 +79,8 @@ class ToolbarComponent extends Card<{ data: Data }> {
collapseItems.push(...group.items);
});
const value = this.getValue();
if (!value || !value.data) return [];
value.data.forEach((group: any) => {
(this.#data || (value ? value.data : []) || []).forEach(
(group: any) => {
const title = group.title;
const items: Array<Omit<CollapseItemProps, 'engine'>> = [];
group.items.forEach((item: any) => {
@ -99,7 +103,8 @@ class ToolbarComponent extends Card<{ data: Data }> {
title,
items,
});
});
},
);
return data;
}
@ -203,7 +208,8 @@ class ToolbarComponent extends Card<{ data: Data }> {
else this.placeholder?.hide();
}
render(): string | void | NodeInterface {
render(data?: any): string | void | NodeInterface {
this.setData(data);
const editor = this.editor;
if (!isEngine(editor) || isServer) return;
const language = editor.language.get<{ placeholder: string }>(

View File

@ -84,10 +84,10 @@ class ToolbarPlugin extends Plugin<Options> {
const data = this.options.config || defaultConfig(this.editor);
const card = this.editor.card.insert(
ToolbarComponent.cardName,
{
{},
data,
},
);
) as ToolbarComponent;
card.setData(data);
this.editor.card.activate(card.root);
range = change.range.get();
//选中关键词输入节点

View File

@ -96,7 +96,6 @@ const CollapseItem: React.FC<CollapseItemProps> = ({
</div>
);
};
return prompt ? (
<Popover
placement={placement || 'right'}

View File

@ -21,6 +21,7 @@ class ToolbarComponent extends Card {
private placeholder?: NodeInterface;
private component?: CollapseComponentInterface;
#collapseData?: Data;
#data?: any;
static get cardName() {
return 'toolbar';
@ -53,6 +54,10 @@ class ToolbarComponent extends Card {
});
}
setData(_data: any) {
this.#data = _data;
}
getData(): Data {
if (!isEngine(this.editor)) {
return [];
@ -72,9 +77,8 @@ class ToolbarComponent extends Card {
collapseItems.push(...group.items);
});
const value = this.getValue();
if (!value || !value['data']) return [];
value['data'].forEach((group: any) => {
(this.#data || (value ? value['data'] : []) || []).forEach(
(group: any) => {
const title = group.title;
const items: Array<Omit<CollapseItemProps, 'engine'>> = [];
group.items.forEach((item: any) => {
@ -97,7 +101,8 @@ class ToolbarComponent extends Card {
title,
items,
});
});
},
);
return data;
}
@ -201,7 +206,8 @@ class ToolbarComponent extends Card {
else this.placeholder?.hide();
}
render(): string | void | NodeInterface {
render(data?: any): string | void | NodeInterface {
this.#data = data;
const editor = this.editor;
if (!isEngine(editor) || isServer) return;
const language = editor.language.get('toolbar', 'component');

View File

@ -1,3 +1,4 @@
import React from 'react';
import {
DATA_TRANSIENT_ELEMENT,
EditorInterface,
@ -86,10 +87,10 @@ class ToolbarPlugin extends Plugin<Options> {
const data = this.options.config || defaultConfig(this.editor);
const card = this.editor.card.insert(
ToolbarComponent.cardName,
{
{},
data,
},
);
) as ToolbarComponent;
card.setData(data);
card.root.attributes(DATA_TRANSIENT_ELEMENT, 'true');
this.editor.card.activate(card.root);
range = change.range.get();

View File

@ -165,13 +165,15 @@ export default class extends Plugin {
card?.getValue() ||
decodeCardValue(node.attributes(CARD_VALUE_KEY));
if (value?.url && value.status === 'done') {
const html = `<a data-type="${
const html = `<span data-type="${
FileComponent.cardName
}" data-value="${encodeCardValue(value)}" href="${
}" data-value="${encodeCardValue(
value,
)}"><a target="_blank" href="${
value.url
}" style="word-wrap: break-word;color: #096DD9;touch-action: manipulation;background-color: rgba(0,0,0,0);text-decoration: none;outline: none;cursor: pointer;transition: color .3s;"><span style="font-size: 14px;">\ud83d\udcce</span>${
value.name
}</a>`;
}</a></span>`;
node.empty();
node.replaceWith($(html));
} else node.remove();

View File

@ -636,13 +636,17 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (this.wrapper) {
// 重新绘制列头部和行头部
const colsHeader = this.wrapper.find(Template.COLS_HEADER_CLASS);
if (value?.cols) {
colsHeader.replaceWith(
$(this.template.renderColsHeader(value?.cols || 0)),
);
}
const rowsHeader = this.wrapper.find(Template.ROWS_HEADER_CLASS);
if (value?.rows) {
rowsHeader.replaceWith(
$(this.template.renderRowsHeader(value?.rows || 0)),
);
}
setTimeout(() => {
// 找到所有可编辑节点,对没有 contenteditable 属性的节点添加contenteditable一下
this.wrapper?.find(EDITABLE_SELECTOR).each((editableNode) => {

View File

@ -1090,6 +1090,7 @@ class TableSelection extends EventEmitter2 implements TableSelectionInterface {
top += rect.top - (vRect?.top || 0) - 13;
left += rect.left - (vRect?.left || 0);
}
const sLeft =
removeUnit(
this.table.wrapper?.find('.data-scrollbar')?.css('left') || '0',

View File

@ -190,9 +190,9 @@ class Template implements TemplateInterface {
noBorder === true ? " data-table-no-border='true'" : ''
} ${DATA_TRANSIENT_ATTRIBUTES}="class">${colgroup}${trs}</table>`;
return `<div class="${TABLE_WRAPPER_CLASS_NAME} ${
return `<div ${DATA_TRANSIENT_ATTRIBUTES}="*" class="${TABLE_WRAPPER_CLASS_NAME} ${
overflow !== false ? TABLE_OVERFLOW_CLASS_NAME : ''
}" ${DATA_TRANSIENT_ATTRIBUTES}="*">${tableHeader}<div class="${VIEWPORT}">${this.renderColsHeader(
}" ${DATA_TRANSIENT_ATTRIBUTES}="*">${tableHeader}<div ${DATA_TRANSIENT_ATTRIBUTES}="*" class="${VIEWPORT}">${this.renderColsHeader(
cols,
)}${table}${placeholder}${tableHighlight}</div>${this.renderRowsHeader(
rows,

View File

@ -67,7 +67,7 @@
{
"id": "kAoP518hzaPYD9Mx9z",
"title": "dsfdf",
"status": "true",
"status": "false",
"children": [
{
"id": 6,
@ -76,5 +76,44 @@
"createdAt": 1639571978834
}
]
},
{
"id": "C7jyDdNy4pWlNZDDVC",
"title": "fffffffffffff",
"status": "false",
"children": [
{
"id": 7,
"username": "G-2",
"content": "dgfg",
"createdAt": 1639653974175
}
]
},
{
"id": "VGAleZwQS7WgYfxX4u",
"title": "ffffffffff",
"status": "false",
"children": [
{
"id": 8,
"username": "G-2",
"content": "dfg",
"createdAt": 1639653998574
}
]
},
{
"id": "753UwqwMh6Rr8HIeKW",
"title": "ghjhgj[card:status,ezXxq]ghjhj",
"status": "false",
"children": [
{
"id": 9,
"username": "G-2",
"content": "ghjhgj",
"createdAt": 1639662957734
}
]
}
]

File diff suppressed because one or more lines are too long