fix: 协同与异步卡片加载会造成写入多个无意义节点
This commit is contained in:
parent
d9ccd8faa5
commit
ba73863f7c
|
@ -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,
|
||||
) => {
|
||||
result: boolean;
|
||||
data: string;
|
||||
parse?: (response: any) => {
|
||||
result: boolean;
|
||||
data: {
|
||||
url: string;
|
||||
preview?: string;
|
||||
download?: string;
|
||||
status?: string;
|
||||
} | string;
|
||||
};
|
||||
```
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
```
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
}
|
||||
|
||||
.editor-content .am-engine {
|
||||
padding: 0 0 60px;
|
||||
padding: 40px 0 60px;
|
||||
|
||||
@media @mobile {
|
||||
padding: 0;
|
||||
|
|
|
@ -333,9 +333,6 @@ const EditorComponent: React.FC<EditorProps> = ({
|
|||
<OTComponent members={members} />,
|
||||
headerOTMembersElement,
|
||||
);
|
||||
return () => {
|
||||
ReactDOM.unmountComponentAtNode(headerOTMembersElement);
|
||||
};
|
||||
}, [members, props.ot]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.data-toc-wrapper {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
min-width: 210px;
|
||||
max-width: 210px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 待创建的需要重新生成节点,并替换当前待创建节点
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' &&
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -170,6 +170,63 @@ class Parser implements ParserInterface {
|
|||
});
|
||||
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) => {
|
||||
if (
|
||||
node.equal(root) ||
|
||||
|
@ -187,61 +244,6 @@ class Parser implements ParserInterface {
|
|||
}
|
||||
}
|
||||
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 节点
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 卡片集合
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,32 +79,32 @@ class ToolbarComponent extends Card<{ data: Data }> {
|
|||
collapseItems.push(...group.items);
|
||||
});
|
||||
const value = this.getValue();
|
||||
if (!value || !value.data) return [];
|
||||
|
||||
value.data.forEach((group: any) => {
|
||||
const title = group.title;
|
||||
const items: Array<Omit<CollapseItemProps, 'engine'>> = [];
|
||||
group.items.forEach((item: any) => {
|
||||
let name = item;
|
||||
if (typeof item !== 'string') name = item.name;
|
||||
const collapseItem = collapseItems.find(
|
||||
(item) => item.name === name,
|
||||
);
|
||||
if (collapseItem) {
|
||||
items.push({
|
||||
...collapseItem,
|
||||
...(typeof item !== 'string' ? item : {}),
|
||||
disabled: collapseItem.onDisabled
|
||||
? collapseItem.onDisabled()
|
||||
: !this.editor.command.queryEnabled(name),
|
||||
});
|
||||
}
|
||||
});
|
||||
data.push({
|
||||
title,
|
||||
items,
|
||||
});
|
||||
});
|
||||
(this.#data || (value ? value.data : []) || []).forEach(
|
||||
(group: any) => {
|
||||
const title = group.title;
|
||||
const items: Array<Omit<CollapseItemProps, 'engine'>> = [];
|
||||
group.items.forEach((item: any) => {
|
||||
let name = item;
|
||||
if (typeof item !== 'string') name = item.name;
|
||||
const collapseItem = collapseItems.find(
|
||||
(item) => item.name === name,
|
||||
);
|
||||
if (collapseItem) {
|
||||
items.push({
|
||||
...collapseItem,
|
||||
...(typeof item !== 'string' ? item : {}),
|
||||
disabled: collapseItem.onDisabled
|
||||
? collapseItem.onDisabled()
|
||||
: !this.editor.command.queryEnabled(name),
|
||||
});
|
||||
}
|
||||
});
|
||||
data.push({
|
||||
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 }>(
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
{},
|
||||
data,
|
||||
) as ToolbarComponent;
|
||||
card.setData(data);
|
||||
this.editor.card.activate(card.root);
|
||||
range = change.range.get();
|
||||
//选中关键词输入节点
|
||||
|
|
|
@ -96,7 +96,6 @@ const CollapseItem: React.FC<CollapseItemProps> = ({
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return prompt ? (
|
||||
<Popover
|
||||
placement={placement || 'right'}
|
||||
|
|
|
@ -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,32 +77,32 @@ class ToolbarComponent extends Card {
|
|||
collapseItems.push(...group.items);
|
||||
});
|
||||
const value = this.getValue();
|
||||
if (!value || !value['data']) return [];
|
||||
|
||||
value['data'].forEach((group: any) => {
|
||||
const title = group.title;
|
||||
const items: Array<Omit<CollapseItemProps, 'engine'>> = [];
|
||||
group.items.forEach((item: any) => {
|
||||
let name = item;
|
||||
if (typeof item !== 'string') name = item.name;
|
||||
const collapseItem = collapseItems.find(
|
||||
(item) => item.name === name,
|
||||
);
|
||||
if (collapseItem) {
|
||||
items.push({
|
||||
...collapseItem,
|
||||
...(typeof item !== 'string' ? item : {}),
|
||||
disabled: collapseItem.onDisabled
|
||||
? collapseItem.onDisabled()
|
||||
: !this.editor.command.queryEnabled(name),
|
||||
});
|
||||
}
|
||||
});
|
||||
data.push({
|
||||
title,
|
||||
items,
|
||||
});
|
||||
});
|
||||
(this.#data || (value ? value['data'] : []) || []).forEach(
|
||||
(group: any) => {
|
||||
const title = group.title;
|
||||
const items: Array<Omit<CollapseItemProps, 'engine'>> = [];
|
||||
group.items.forEach((item: any) => {
|
||||
let name = item;
|
||||
if (typeof item !== 'string') name = item.name;
|
||||
const collapseItem = collapseItems.find(
|
||||
(item) => item.name === name,
|
||||
);
|
||||
if (collapseItem) {
|
||||
items.push({
|
||||
...collapseItem,
|
||||
...(typeof item !== 'string' ? item : {}),
|
||||
disabled: collapseItem.onDisabled
|
||||
? collapseItem.onDisabled()
|
||||
: !this.editor.command.queryEnabled(name),
|
||||
});
|
||||
}
|
||||
});
|
||||
data.push({
|
||||
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');
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
{},
|
||||
data,
|
||||
) as ToolbarComponent;
|
||||
card.setData(data);
|
||||
card.root.attributes(DATA_TRANSIENT_ELEMENT, 'true');
|
||||
this.editor.card.activate(card.root);
|
||||
range = change.range.get();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -636,13 +636,17 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
if (this.wrapper) {
|
||||
// 重新绘制列头部和行头部
|
||||
const colsHeader = this.wrapper.find(Template.COLS_HEADER_CLASS);
|
||||
colsHeader.replaceWith(
|
||||
$(this.template.renderColsHeader(value?.cols || 0)),
|
||||
);
|
||||
if (value?.cols) {
|
||||
colsHeader.replaceWith(
|
||||
$(this.template.renderColsHeader(value?.cols || 0)),
|
||||
);
|
||||
}
|
||||
const rowsHeader = this.wrapper.find(Template.ROWS_HEADER_CLASS);
|
||||
rowsHeader.replaceWith(
|
||||
$(this.template.renderRowsHeader(value?.rows || 0)),
|
||||
);
|
||||
if (value?.rows) {
|
||||
rowsHeader.replaceWith(
|
||||
$(this.template.renderRowsHeader(value?.rows || 0)),
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
// 找到所有可编辑节点,对没有 contenteditable 属性的节点添加contenteditable一下
|
||||
this.wrapper?.find(EDITABLE_SELECTOR).each((editableNode) => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue