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 the uploaded Respone and return result: whether it is successful or not, data: success: file address, failure: error message
*/ */
parse?: ( parse?: (response: any) => {
response: any, result: boolean;
) => { data: {
result: boolean; url: string;
data: string; preview?: string;
download?: string;
status?: string;
} | string;
}; };
``` ```

View File

@ -124,11 +124,14 @@ limitSize?: number;
/** /**
* 解析上传后的Respone返回 result:是否成功data:成功:文件地址,失败:错误信息 * 解析上传后的Respone返回 result:是否成功data:成功:文件地址,失败:错误信息
*/ */
parse?: ( parse?: (response: any) => {
response: any,
) => {
result: boolean; 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 rootRect = root.get<HTMLElement>()!.getBoundingClientRect();
const top = rangeRect.y - rootRect.y; const top = rangeRect.y - rootRect.y;
this.#container.css('top', `${top}px`); this.#container.css('top', `${top}px`);
this.#container.css('right', `16px`); this.#container.css('right', `-16px`);
this.#container.show('flex'); this.#container.show('flex');
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -537,7 +537,9 @@ class NativeEvent {
) => void, ) => void,
) { ) {
const { change } = this.engine; 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); this.engine.trigger('paste:before', fragment);
if (insert) insert(fragment, range, undefined, followActiveMark); if (insert) insert(fragment, range, undefined, followActiveMark);
else else

View File

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

View File

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

View File

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

View File

@ -42,6 +42,15 @@ class Consumer implements ConsumerInterface {
endOffset: index, endOffset: index,
}; };
const offset = index - JSON0_INDEX.ELEMENT; 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 = Array.from(node.childNodes).filter((node) => {
const childNode = $(node); const childNode = $(node);
return !isTransientElement(childNode); return !isTransientElement(childNode);
@ -51,7 +60,7 @@ class Consumer implements ConsumerInterface {
1 === path.length || 1 === path.length ||
pathOffset === JSON0_INDEX.TAG_NAME || pathOffset === JSON0_INDEX.TAG_NAME ||
pathOffset === JSON0_INDEX.ATTRIBUTE || pathOffset === JSON0_INDEX.ATTRIBUTE ||
childNode.nodeType === Node.TEXT_NODE (childNode && childNode.nodeType === Node.TEXT_NODE)
) { ) {
return { return {
startNode: childNode, startNode: childNode,

View File

@ -14,7 +14,7 @@ import {
updateIndex, updateIndex,
opsSort, opsSort,
} from './utils'; } from './utils';
import { escapeDots, escape } from '../utils/string'; import { escapeDots, escape, decodeCardValue } from '../utils/string';
import { toJSON0, getValue } from './utils'; import { toJSON0, getValue } from './utils';
import { EngineInterface } from '../types/engine'; import { EngineInterface } from '../types/engine';
import { Op, Path, StringInsertOp, StringDeleteOp, Doc } from 'sharedb'; import { Op, Path, StringInsertOp, StringDeleteOp, Doc } from 'sharedb';
@ -22,15 +22,20 @@ import { NodeInterface } from '../types/node';
import { DocInterface, RepairOp } from '../types/ot'; import { DocInterface, RepairOp } from '../types/ot';
import { $ } from '../node'; import { $ } from '../node';
import { import {
CARD_ELEMENT_KEY, CARD_CENTER_SELECTOR,
CARD_KEY, CARD_KEY,
CARD_LOADING_KEY, CARD_LOADING_KEY,
CARD_SELECTOR,
CARD_VALUE_KEY,
DATA_ELEMENT,
DATA_ID, DATA_ID,
DATA_TRANSIENT_ELEMENT,
JSON0_INDEX, JSON0_INDEX,
UI,
UI_SELECTOR, UI_SELECTOR,
} from '../constants'; } from '../constants';
import { getDocument } from '../utils/node'; import { getDocument } from '../utils/node';
import { CardValue } from 'src'; import type { CardEntry } from '../types/card';
class Producer extends EventEmitter2 { class Producer extends EventEmitter2 {
private engine: EngineInterface; private engine: EngineInterface;
@ -519,18 +524,24 @@ class Producer extends EventEmitter2 {
const tMapValue = cardMap.get(cardElement[0]); const tMapValue = cardMap.get(cardElement[0]);
if (tMapValue === undefined && cardElement.isEditableCard()) { if (tMapValue === undefined && cardElement.isEditableCard()) {
const cardName = cardElement.attributes(CARD_KEY); const cardName = cardElement.attributes(CARD_KEY);
const cardValue = decodeCardValue(
cardElement.attributes(CARD_VALUE_KEY),
);
const result = this.findCardForDoc( const result = this.findCardForDoc(
this.doc.data, this.doc.data,
cardName, cardName,
(attriables) => { (attriables) => {
return ( // 卡片id一致
attriables[DATA_ID] === const value = decodeCardValue(
cardElement.attributes(DATA_ID) attriables[CARD_VALUE_KEY],
); );
return value.id === cardValue.id;
}, },
); );
// 没有这个卡片节点,或者卡片内部已经渲染了才需要过滤
if ( if (
!result?.rendered && result &&
!result.rendered &&
cardElement.attributes(CARD_LOADING_KEY) !== 'remote' cardElement.attributes(CARD_LOADING_KEY) !== 'remote'
) { ) {
isTransient = false; isTransient = false;
@ -542,6 +553,83 @@ class Producer extends EventEmitter2 {
} else if (tMapValue !== undefined) { } else if (tMapValue !== undefined) {
isTransient = tMapValue; 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 ( if (
!isTransient && !isTransient &&
@ -564,7 +652,6 @@ class Producer extends EventEmitter2 {
return !isTransient; return !isTransient;
}); });
this.clearCache(); this.clearCache();
//重置缓存 //重置缓存
this.cacheTransientElements = undefined; this.cacheTransientElements = undefined;
let ops = this.generateOps(records); let ops = this.generateOps(records);

View File

@ -170,6 +170,63 @@ class Parser implements ParserInterface {
}); });
return; return;
} }
// 过滤分割
const filter = (node: NodeInterface, type: string = 'mark') => {
//获取节点属性样式
const attributes = node.attributes();
const style = getStyleMap(attributes.style || '');
delete attributes.style;
if (
Object.keys(attributes).length === 0 &&
Object.keys(style).length === 0
)
return;
//过滤不符合当前节点规则的属性样式
schema.filter(node, attributes, style);
//复制一个节点
const newNode = node.clone();
//移除 data-id以免在下次判断类型的时候使用缓存
newNode.removeAttributes(DATA_ID);
//移除符合当前节点的属性样式,剩余的属性样式组成新的节点
const attrKeys = Object.keys(attributes);
let filterAttrCount = 0;
attrKeys.forEach((name) => {
if (attributes[name]) {
filterAttrCount++;
newNode.removeAttributes(name);
}
});
let filterStyleCount = 0;
const styleKeys = Object.keys(style);
styleKeys.forEach((name) => {
if (style[name]) {
filterStyleCount++;
newNode.css(name, '');
}
});
// 如果这个节点过滤掉所有属性样式后还是一个有效的节点就替换掉当前节点
if (
filterAttrCount === attrKeys.length &&
filterStyleCount === styleKeys.length &&
schema.getType(newNode) === type
) {
node.before(newNode);
const children = node.children();
newNode.append(
children.length > 0
? children
: type === 'block'
? '<br />'
: $('\u200b', null),
);
node.remove();
node = newNode;
return;
}
if (newNode.attributes('style').trim() === '')
newNode.removeAttributes('style');
return newNode;
};
root.traverse((node) => { root.traverse((node) => {
if ( if (
node.equal(root) || node.equal(root) ||
@ -187,61 +244,6 @@ class Parser implements ParserInterface {
} }
} }
if (isCard) return; if (isCard) return;
//分割
const filter = (node: NodeInterface) => {
//获取节点属性样式
const attributes = node.attributes();
const style = getStyleMap(attributes.style || '');
delete attributes.style;
if (
Object.keys(attributes).length === 0 &&
Object.keys(style).length === 0
)
return;
//过滤不符合当前节点规则的属性样式
schema.filter(node, attributes, style);
//复制一个节点
const newNode = node.clone();
//移除 data-id以免在下次判断类型的时候使用缓存
newNode.removeAttributes(DATA_ID);
//移除符合当前节点的属性样式,剩余的属性样式组成新的节点
const attrKeys = Object.keys(attributes);
let filterAttrCount = 0;
attrKeys.forEach((name) => {
if (attributes[name]) {
filterAttrCount++;
newNode.removeAttributes(name);
}
});
let filterStyleCount = 0;
const styleKeys = Object.keys(style);
styleKeys.forEach((name) => {
if (style[name]) {
filterStyleCount++;
newNode.css(name, '');
}
});
// 如果这个节点过滤掉所有属性样式后还是一个有效的节点就替换掉当前节点
if (
filterAttrCount === attrKeys.length &&
filterStyleCount === styleKeys.length &&
schema.getType(newNode) === 'mark'
) {
node.before(newNode);
const children = node.children();
newNode.append(
children.length > 0 ? children : $('\u200b', null),
);
node.remove();
node = newNode;
return;
}
if (newNode.attributes('style').trim() === '')
newNode.removeAttributes('style');
return newNode;
};
//当前节点是 inline 节点inline 节点不允许嵌套、不允许放入mark节点
inlineApi.flat(node, schema);
//当前节点是 mark 节点 //当前节点是 mark 节点
if (nodeApi.isMark(node, schema)) { if (nodeApi.isMark(node, schema)) {
//过滤掉当前mark节点属性样式并使用剩下的属性样式组成新的节点 //过滤掉当前mark节点属性样式并使用剩下的属性样式组成新的节点
@ -285,6 +287,9 @@ class Parser implements ParserInterface {
oldRules.push(rule); 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.find(`[${DATA_ID}="${path.start.id}"]`)
: root; : root;
const startNode = getNode( const startNode = getNode(
path.start.bi > -1 ? startPath.slice(path.start.bi) : startPath, path.start.bi > -1 && beginContext.length > 0
beginContext, ? startPath.slice(path.start.bi)
: startPath,
beginContext.length > 0 ? beginContext : undefined,
); );
const endContext = path.end.id const endContext = path.end.id
? root.find(`[${DATA_ID}="${path.end.id}"]`) ? root.find(`[${DATA_ID}="${path.end.id}"]`)
: root; : root;
const endNode = getNode( const endNode = getNode(
path.end.bi > -1 ? endPath.slice(path.end.bi) : endPath, path.end.bi > -1 && endContext.length > 0
endContext, ? endPath.slice(path.end.bi)
: endPath,
endContext.length > 0 ? endContext : undefined,
); );
const range = Range.create(editor, document); const range = Range.create(editor, document);
setRange( setRange(

View File

@ -320,12 +320,9 @@ class Schema implements SchemaInterface {
* @param rule * @param rule
*/ */
filterStyles(styles: { [k: string]: string }, rule: SchemaRule) { filterStyles(styles: { [k: string]: string }, rule: SchemaRule) {
if (!rule.attributes?.style) {
styles = {};
return;
}
Object.keys(styles).forEach((styleName) => { Object.keys(styles).forEach((styleName) => {
if ( if (
!rule.attributes?.style ||
!this.checkValue( !this.checkValue(
rule.attributes!.style as SchemaAttributes, rule.attributes!.style as SchemaAttributes,
styleName, styleName,
@ -342,12 +339,9 @@ class Schema implements SchemaInterface {
* @param rule * @param rule
*/ */
filterAttributes(attributes: { [k: string]: string }, rule: SchemaRule) { filterAttributes(attributes: { [k: string]: string }, rule: SchemaRule) {
if (!rule.attributes) {
attributes = {};
return;
}
Object.keys(attributes).forEach((attributesName) => { Object.keys(attributes).forEach((attributesName) => {
if ( if (
!rule.attributes ||
!this.checkValue( !this.checkValue(
rule.attributes as SchemaAttributes, rule.attributes as SchemaAttributes,
attributesName, attributesName,

View File

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

View File

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

View File

@ -1,8 +1,10 @@
import { isMobile } from '../../utils';
import Default from './default'; import Default from './default';
class At extends Default { class At extends Default {
hotkey = (event: KeyboardEvent) => hotkey = (event: KeyboardEvent) =>
event.key === '@' || 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; export default At;

View File

@ -23,6 +23,7 @@ class ToolbarComponent extends Card<{ data: Data }> {
private placeholder?: NodeInterface; private placeholder?: NodeInterface;
private component?: CollapseComponentInterface; private component?: CollapseComponentInterface;
#collapseData?: Data; #collapseData?: Data;
#data?: any;
static get cardName() { static get cardName() {
return 'toolbar'; return 'toolbar';
@ -55,6 +56,10 @@ class ToolbarComponent extends Card<{ data: Data }> {
}); });
} }
setData(_data: any) {
this.#data = _data;
}
getData(): Data { getData(): Data {
if (!isEngine(this.editor)) { if (!isEngine(this.editor)) {
return []; return [];
@ -74,32 +79,32 @@ class ToolbarComponent extends Card<{ data: Data }> {
collapseItems.push(...group.items); collapseItems.push(...group.items);
}); });
const value = this.getValue(); const value = this.getValue();
if (!value || !value.data) return []; (this.#data || (value ? value.data : []) || []).forEach(
(group: any) => {
value.data.forEach((group: any) => { const title = group.title;
const title = group.title; const items: Array<Omit<CollapseItemProps, 'engine'>> = [];
const items: Array<Omit<CollapseItemProps, 'engine'>> = []; group.items.forEach((item: any) => {
group.items.forEach((item: any) => { let name = item;
let name = item; if (typeof item !== 'string') name = item.name;
if (typeof item !== 'string') name = item.name; const collapseItem = collapseItems.find(
const collapseItem = collapseItems.find( (item) => item.name === name,
(item) => item.name === name, );
); if (collapseItem) {
if (collapseItem) { items.push({
items.push({ ...collapseItem,
...collapseItem, ...(typeof item !== 'string' ? item : {}),
...(typeof item !== 'string' ? item : {}), disabled: collapseItem.onDisabled
disabled: collapseItem.onDisabled ? collapseItem.onDisabled()
? collapseItem.onDisabled() : !this.editor.command.queryEnabled(name),
: !this.editor.command.queryEnabled(name), });
}); }
} });
}); data.push({
data.push({ title,
title, items,
items, });
}); },
}); );
return data; return data;
} }
@ -203,7 +208,8 @@ class ToolbarComponent extends Card<{ data: Data }> {
else this.placeholder?.hide(); else this.placeholder?.hide();
} }
render(): string | void | NodeInterface { render(data?: any): string | void | NodeInterface {
this.setData(data);
const editor = this.editor; const editor = this.editor;
if (!isEngine(editor) || isServer) return; if (!isEngine(editor) || isServer) return;
const language = editor.language.get<{ placeholder: string }>( 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 data = this.options.config || defaultConfig(this.editor);
const card = this.editor.card.insert( const card = this.editor.card.insert(
ToolbarComponent.cardName, ToolbarComponent.cardName,
{ {},
data, data,
}, ) as ToolbarComponent;
); card.setData(data);
this.editor.card.activate(card.root); this.editor.card.activate(card.root);
range = change.range.get(); range = change.range.get();
//选中关键词输入节点 //选中关键词输入节点

View File

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

View File

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

View File

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

View File

@ -165,13 +165,15 @@ export default class extends Plugin {
card?.getValue() || card?.getValue() ||
decodeCardValue(node.attributes(CARD_VALUE_KEY)); decodeCardValue(node.attributes(CARD_VALUE_KEY));
if (value?.url && value.status === 'done') { if (value?.url && value.status === 'done') {
const html = `<a data-type="${ const html = `<span data-type="${
FileComponent.cardName FileComponent.cardName
}" data-value="${encodeCardValue(value)}" href="${ }" data-value="${encodeCardValue(
value,
)}"><a target="_blank" href="${
value.url 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>${ }" 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 value.name
}</a>`; }</a></span>`;
node.empty(); node.empty();
node.replaceWith($(html)); node.replaceWith($(html));
} else node.remove(); } else node.remove();

View File

@ -636,13 +636,17 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (this.wrapper) { if (this.wrapper) {
// 重新绘制列头部和行头部 // 重新绘制列头部和行头部
const colsHeader = this.wrapper.find(Template.COLS_HEADER_CLASS); const colsHeader = this.wrapper.find(Template.COLS_HEADER_CLASS);
colsHeader.replaceWith( if (value?.cols) {
$(this.template.renderColsHeader(value?.cols || 0)), colsHeader.replaceWith(
); $(this.template.renderColsHeader(value?.cols || 0)),
);
}
const rowsHeader = this.wrapper.find(Template.ROWS_HEADER_CLASS); const rowsHeader = this.wrapper.find(Template.ROWS_HEADER_CLASS);
rowsHeader.replaceWith( if (value?.rows) {
$(this.template.renderRowsHeader(value?.rows || 0)), rowsHeader.replaceWith(
); $(this.template.renderRowsHeader(value?.rows || 0)),
);
}
setTimeout(() => { setTimeout(() => {
// 找到所有可编辑节点,对没有 contenteditable 属性的节点添加contenteditable一下 // 找到所有可编辑节点,对没有 contenteditable 属性的节点添加contenteditable一下
this.wrapper?.find(EDITABLE_SELECTOR).each((editableNode) => { 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; top += rect.top - (vRect?.top || 0) - 13;
left += rect.left - (vRect?.left || 0); left += rect.left - (vRect?.left || 0);
} }
const sLeft = const sLeft =
removeUnit( removeUnit(
this.table.wrapper?.find('.data-scrollbar')?.css('left') || '0', 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'" : '' noBorder === true ? " data-table-no-border='true'" : ''
} ${DATA_TRANSIENT_ATTRIBUTES}="class">${colgroup}${trs}</table>`; } ${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 : '' 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, cols,
)}${table}${placeholder}${tableHighlight}</div>${this.renderRowsHeader( )}${table}${placeholder}${tableHighlight}</div>${this.renderRowsHeader(
rows, rows,

View File

@ -67,7 +67,7 @@
{ {
"id": "kAoP518hzaPYD9Mx9z", "id": "kAoP518hzaPYD9Mx9z",
"title": "dsfdf", "title": "dsfdf",
"status": "true", "status": "false",
"children": [ "children": [
{ {
"id": 6, "id": 6,
@ -76,5 +76,44 @@
"createdAt": 1639571978834 "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