From 1098559fbb6fa0aaa9292b6fcae86d6cb0a086aa Mon Sep 17 00:00:00 2001 From: yanmao Date: Mon, 27 Sep 2021 00:48:17 +0800 Subject: [PATCH] fix(ot): submit ops error - optimize placeholder display timing - data-element=ui node is not correctly filtered when submitting ops in collaborative interaction --- packages/engine/src/change/event.ts | 10 +++- packages/engine/src/change/index.ts | 10 ++++ packages/engine/src/engine/container.ts | 4 +- packages/engine/src/engine/index.ts | 8 +++ packages/engine/src/ot/applier.ts | 4 +- packages/engine/src/ot/creator.ts | 78 +++++++++++++------------ packages/engine/src/ot/mutation.ts | 9 +++ packages/engine/src/types/engine.ts | 8 +++ plugins/table/src/component/helper.ts | 4 +- site-ssr/app/data/doc.json | 2 +- 10 files changed, 93 insertions(+), 44 deletions(-) diff --git a/packages/engine/src/change/event.ts b/packages/engine/src/change/event.ts index 096a230b7..5c1883138 100644 --- a/packages/engine/src/change/event.ts +++ b/packages/engine/src/change/event.ts @@ -68,6 +68,7 @@ class ChangeEvent implements ChangeEventInterface { return; } if (!this.isCardInput(event)) { + console.log('compositionstart'); this.engine.ot.startMutationCache(); } // 组合输入法缓存协同 @@ -95,6 +96,7 @@ class ChangeEvent implements ChangeEventInterface { if (this.engine.readonly) { return; } + console.log('compositionend'); this.isComposing = false; }); //对系统工具栏操作拦截,一般针对移动端的文本上下文工具栏 @@ -150,6 +152,7 @@ class ChangeEvent implements ChangeEventInterface { const { inputType } = event; // 在组合输入法未正常执行结束命令插入就先提交协同 if (this.isComposing && !inputType.includes('Composition')) { + console.log('beforeinput', inputType); this.engine.ot.submitMutationCache(); } const commandTypes = ['format', 'history']; @@ -172,10 +175,15 @@ class ChangeEvent implements ChangeEventInterface { if (this.isCardInput(e)) { return; } - + if (this.engine.isEmpty()) { + this.engine.showPlaceholder(); + } else { + this.engine.hidePlaceholder(); + } if (inputTimeout) clearTimeout(inputTimeout); inputTimeout = setTimeout(() => { if (!this.isComposing) { + console.log('input'); callback(e); // 组合输入法结束后提交协同 this.engine.ot.submitMutationCache(); diff --git a/packages/engine/src/change/index.ts b/packages/engine/src/change/index.ts index 7b3b0730e..cc874e066 100644 --- a/packages/engine/src/change/index.ts +++ b/packages/engine/src/change/index.ts @@ -273,6 +273,10 @@ class ChangeModel implements ChangeInterface { endNode.append('
'); } } + + if (startNode.isEditable() && startNode.children().length === 0) { + startNode.html('


'); + } //在非折叠,或者当前range对象和selection中的对象不一致的时候重新设置range if ( selection && @@ -1362,6 +1366,12 @@ class ChangeModel implements ChangeInterface { } // 先删除范围内的所有内容 safeRange.extractContents(); + if ( + safeRange.startNode.isEditable() && + safeRange.startNode.children().length === 0 + ) { + safeRange.startNode.html('


'); + } safeRange.collapse(true); // 后续处理 const { startNode } = safeRange diff --git a/packages/engine/src/engine/container.ts b/packages/engine/src/engine/container.ts index 40da3b229..e64def34c 100644 --- a/packages/engine/src/engine/container.ts +++ b/packages/engine/src/engine/container.ts @@ -133,7 +133,7 @@ class Container { showPlaceholder() { const { placeholder } = this.options; if (placeholder) { - if (this.#styleElement) + if (this.#styleElement && this.#styleElement.parentNode) document.body.removeChild(this.#styleElement); this.#styleElement = document.createElement('style'); //const left = this.node.css('padding-left'); @@ -150,7 +150,7 @@ class Container { this.node.attributes({ 'data-placeholder': placeholder, }); - } else if (this.#styleElement) + } else if (this.#styleElement && this.#styleElement.parentNode) document.body.removeChild(this.#styleElement); } diff --git a/packages/engine/src/engine/index.ts b/packages/engine/src/engine/index.ts index 94c804280..e63b99cd3 100644 --- a/packages/engine/src/engine/index.ts +++ b/packages/engine/src/engine/index.ts @@ -451,6 +451,14 @@ class Engine implements EngineInterface { console.log(`confirm:${message}`); return Promise.reject(false); } + + showPlaceholder() { + this._container.showPlaceholder(); + } + + hidePlaceholder() { + this._container.hidePlaceholder(); + } } export default Engine; diff --git a/packages/engine/src/ot/applier.ts b/packages/engine/src/ot/applier.ts index 901193876..712a4fa7c 100644 --- a/packages/engine/src/ot/applier.ts +++ b/packages/engine/src/ot/applier.ts @@ -235,8 +235,10 @@ class Applier implements ApplierInterface { nodeValue.substring(offset); if (begine && begine.parentNode === end) begine.nodeValue = value; - else { + else if (!!value) { const textNode = document.createTextNode(value); + if (end.firstChild?.nodeName === 'BR') + end.firstChild.remove(); end.insertBefore(textNode, end.firstChild); } return node; diff --git a/packages/engine/src/ot/creator.ts b/packages/engine/src/ot/creator.ts index 7e3cfd523..faefd90d5 100644 --- a/packages/engine/src/ot/creator.ts +++ b/packages/engine/src/ot/creator.ts @@ -24,6 +24,7 @@ import { $ } from '../node'; import { CARD_ASYNC_RENDER, DATA_ELEMENT, + DATA_TRANSIENT_ELEMENT, ROOT, UI_SELECTOR, } from '../constants'; @@ -247,7 +248,7 @@ class Creator extends EventEmitter2 { attrOps.push({ path, oldPath, - newValue: target['data'], + newValue: record['text-data'] || target['data'], }); isDataString = true; } @@ -422,13 +423,14 @@ class Creator extends EventEmitter2 { } }); //所有的UI子节点 - container - .find(`${UI_SELECTOR}`) - .allChildren() - .forEach((child) => { + const uiElements = container.find(`${UI_SELECTOR}`); + uiElements.each((_, index) => { + const ui = uiElements.eq(index); + ui?.allChildren().forEach((child) => { if (child.type === getDocument().ELEMENT_NODE) this.cacheTransientElements?.push(child[0]); }); + }); } records = records.filter( (record) => @@ -445,39 +447,39 @@ class Creator extends EventEmitter2 { } normalizeOps(ops: Op[]) { - if (this.engine.change.isComposing()) { - if ( - ops.length === 2 && - 'ld' in ops[1] && - ops[1].ld[0] && - 'li' in ops[0] && - typeof ops[0].li === 'string' - ) { - this.lineStart = true; - ops.splice(0, 1); - //ops[0].li = ''; - this.readyToEmitOps(ops); - return; - } - if (ops.length === 1 && isCursorOp(ops[0])) return; - if (ops.length === 2 && 'si' in ops[0] && isEqual(ops[0], ops[1])) - ops.splice(1, 1); - this.laterOps = ops; - if (this.timer) clearTimeout(this.timer); - this.timer = setTimeout(() => { - if (!this.engine.change.isComposing()) { - if (this.lineStart) { - this.engine.history.startCache(10); - this.lineStart = false; - } - if (this.laterOps) { - this.readyToEmitOps(this.laterOps); - this.laterOps = null; - } - this.timer = null; - } - }, 0); - } + // if (this.engine.change.isComposing()) { + // if ( + // ops.length === 2 && + // 'ld' in ops[1] && + // ops[1].ld[0] && + // 'li' in ops[0] && + // typeof ops[0].li === 'string' + // ) { + // this.lineStart = true; + // ops.splice(0, 1); + // if(ops[0].li) ops[0].li = ''; + // this.readyToEmitOps(ops); + // return; + // } + // if (ops.length === 1 && isCursorOp(ops[0])) return; + // if (ops.length === 2 && 'si' in ops[0] && isEqual(ops[0], ops[1])) + // ops.splice(1, 1); + // this.laterOps = ops; + // if (this.timer) clearTimeout(this.timer); + // this.timer = setTimeout(() => { + // if (!this.engine.change.isComposing()) { + // if (this.lineStart) { + // this.engine.history.startCache(10); + // this.lineStart = false; + // } + // if (this.laterOps) { + // this.readyToEmitOps(this.laterOps); + // this.laterOps = null; + // } + // this.timer = null; + // } + // }, 0); + // } if (this.laterOps) { const equal = isEqual(this.laterOps, ops); this.readyToEmitOps(this.laterOps); diff --git a/packages/engine/src/ot/mutation.ts b/packages/engine/src/ot/mutation.ts index eb13dcff5..2ccf09395 100644 --- a/packages/engine/src/ot/mutation.ts +++ b/packages/engine/src/ot/mutation.ts @@ -84,7 +84,16 @@ class Mutation extends EventEmitter2 implements MutationInterface { submitCache() { if (this.isCache) { setTimeout(() => { + if (this.engine.change.isComposing()) return; this.isCache = false; + this.cache = this.cache.map((record) => { + if (record.type === 'characterData') { + if (record.target.nodeType === document.TEXT_NODE) { + record['text-data'] = record.target.textContent; + } + } + return record; + }); if (this.cache.length > 0) this.creator.handleMutations(this.cache); this.cache = []; diff --git a/packages/engine/src/types/engine.ts b/packages/engine/src/types/engine.ts index 71c988801..ef8602e47 100644 --- a/packages/engine/src/types/engine.ts +++ b/packages/engine/src/types/engine.ts @@ -534,6 +534,14 @@ export interface EngineInterface extends EditorInterface { * 获取JSON格式的值 */ getJsonValue(): string | undefined | (string | {})[]; + /** + * 展示 placeholder + */ + showPlaceholder(): void; + /** + * 隐藏 placeholder + */ + hidePlaceholder(): void; /** * 绑定事件 * @param eventType 事件类型 diff --git a/plugins/table/src/component/helper.ts b/plugins/table/src/component/helper.ts index c87ca907c..ba810cf83 100644 --- a/plugins/table/src/component/helper.ts +++ b/plugins/table/src/component/helper.ts @@ -526,7 +526,9 @@ class Helper implements HelperInterface { const colElement = cols[c] as HTMLTableColElement; const _width = cols.eq(c)?.attributes('width'); if (_width) { - cols.eq(c)?.attributes('width', parseInt(_width)); + const widthValue = parseInt(_width); + if (widthValue !== NaN) + cols.eq(c)?.attributes('width', widthValue); } if (colElement.span > 1) { diff --git a/site-ssr/app/data/doc.json b/site-ssr/app/data/doc.json index 4e4d87df6..a55d6dd84 100644 --- a/site-ssr/app/data/doc.json +++ b/site-ssr/app/data/doc.json @@ -1,7 +1,7 @@ { "id": "demo", "content": { - "value": "

sdf

sdfdfs

jjj

sdf

dfdsd

sdfsdfsdfdsfdfsdfdgdfgfdgasdsfsdfdsfsdfdfdfgfg

dsdfdfsdfsdfsdfsdfdsfdfdfgsdfdsfdsfdgfgfdgfdgdfgfdgfdgfg

sdhfgbbggddgggs似懂非懂分

sdfdsfdsgfdgsdfsfdsdfdfgfvdfgfdgfgfgfsdfdfdfffffffffffffffgfdgdfghgfh


ghgfhgsdfdfdfdffhgfh




", + "value": "

132

", "paths": [] } }