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,
) => {
result: boolean;
data: string;
parse?: (response: any) => {
result: boolean;
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,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);
}
}
});

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,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 }>(

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,
},
);
{},
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,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');

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,
},
);
{},
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);
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) => {

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