diff --git a/examples/react/components/editor/config.tsx b/examples/react/components/editor/config.tsx index 9e5d1508..6d5f230c 100644 --- a/examples/react/components/editor/config.tsx +++ b/examples/react/components/editor/config.tsx @@ -128,7 +128,7 @@ export const pluginConfig: { [key: string]: PluginOptions } = { // 减去大纲的宽度 const width = editorLeft - $('.data-toc-wrapper').width(); // 留 16px 的间隔 - return width <= 0 ? 100 : width - 16; + return width <= 0 ? 0 : width - 16; }, maxRightWidth: () => { // 编辑区域位置 @@ -139,7 +139,7 @@ export const pluginConfig: { [key: string]: PluginOptions } = { // 减去评论区域的宽度 const width = editorRigth - $('.doc-comment-layer').width(); // 留 16px 的间隔 - return width <= 0 ? 100 : width - 16; + return width <= 0 ? 0 : width - 16; }, }, }, diff --git a/packages/engine/src/change/index.ts b/packages/engine/src/change/index.ts index ea74b8da..34406f26 100644 --- a/packages/engine/src/change/index.ts +++ b/packages/engine/src/change/index.ts @@ -399,14 +399,20 @@ class ChangeModel implements ChangeInterface { }; } let nextNode = firstNode.next(); + let beforeNode = firstNode; nodeApi.insert(firstNode, range); while (nextNode && !nodeApi.isBlock(nextNode)) { if (range.startContainer.nodeType === Node.TEXT_NODE) range.enlargeToElementNode().collapse(false); const newNext = nextNode.next(); - nodeApi.insert(nextNode, range); + beforeNode.after(nextNode); + beforeNode = nextNode; + //nodeApi.insert(nextNode, range); nextNode = newNext; } + if (beforeNode !== firstNode) { + range.select(beforeNode, true).collapse(false); + } if (childNodes.length === 0) { apply(range); return; diff --git a/packages/engine/src/change/paste.ts b/packages/engine/src/change/paste.ts index 1b9a6a91..00e04b3c 100644 --- a/packages/engine/src/change/paste.ts +++ b/packages/engine/src/change/paste.ts @@ -351,7 +351,8 @@ export default class Paste { const nextReg = node.get()!.splitText(match.index); const endReg = nextReg.splitText(match[0].length); node.after(nextReg); - node.after(endReg); + nextReg.after(endReg); + if (!node.text()) node.remove(); } } // 删除包含Card的 pre 标签 diff --git a/packages/engine/src/node/entry.ts b/packages/engine/src/node/entry.ts index 0199bf11..d853ee39 100644 --- a/packages/engine/src/node/entry.ts +++ b/packages/engine/src/node/entry.ts @@ -1038,22 +1038,11 @@ class NodeEntry implements NodeInterface { innerHeight: 0, innerWidth: 0, }; - let top, left, bottom, right; - if (this.length > 0) { - const element = this.get()!; - const rect = element.getBoundingClientRect(); - top = rect.top; - left = rect.left; - bottom = rect.bottom; - right = rect.right; - } else { - const element = this.get()!; - const rect = element.getBoundingClientRect(); - top = rect.top; - left = rect.left; - bottom = rect.bottom; - right = rect.right; - } + + const element = this.get()!; + const rect = element.getBoundingClientRect(); + const { top, left, bottom, right } = rect; + return { top, left, @@ -1081,10 +1070,15 @@ class NodeEntry implements NodeInterface { if (viewNode) viewNode.parentNode?.removeChild(viewNode); // 简单模式,只判断任一方向是否在视口内 if (simpleMode) { - return top <= vp.bottom || bottom <= vp.bottom; + return ( + (top > 0 && top <= vp.bottom) || + (bottom > 0 && bottom <= vp.bottom) + ); } return ( + top > 0 && top >= vp.top && + left > 0 && left >= vp.left && bottom <= vp.bottom && right <= vp.right diff --git a/packages/engine/src/node/index.ts b/packages/engine/src/node/index.ts index aa07314d..d12bfb70 100644 --- a/packages/engine/src/node/index.ts +++ b/packages/engine/src/node/index.ts @@ -827,7 +827,6 @@ class NodeModel implements NodeModelInterface { isBegin = true; if (text.length === 0) { childNode.remove(); - break; } } //移除末尾换行符 @@ -836,11 +835,11 @@ class NodeModel implements NodeModelInterface { childNode.text(text.substr(0, match.index)); cloneNode.append(childNode); break; - } else if (isBegin) { + } else if (isBegin && childNode.length > 0) { childNode.text(text); } } - cloneNode.append(childNode); + if (childNode.length > 0) cloneNode.append(childNode); //判断下一个节点的开头是换行符,有换行符就跳出 if (nextNode?.isText()) { const text = nextNode.text(); diff --git a/packages/engine/src/parser/index.ts b/packages/engine/src/parser/index.ts index 44756a86..3e67ca6a 100644 --- a/packages/engine/src/parser/index.ts +++ b/packages/engine/src/parser/index.ts @@ -501,9 +501,9 @@ class Parser implements ParserInterface { if (result.length > 0 && /^\n+/g.test(result[0])) { result[0] = result[0].replace(/^\n+/g, ''); } - if (result.length > 0 && /^\n+/g.test(result[result.length - 1])) { + if (result.length > 0 && /\n+$/g.test(result[result.length - 1])) { result[result.length - 1] = result[result.length - 1].replace( - /^\n+/g, + /\n+$/g, '', ); } diff --git a/packages/engine/src/scrollbar/index.ts b/packages/engine/src/scrollbar/index.ts index 95ab7eaa..d8a2c926 100644 --- a/packages/engine/src/scrollbar/index.ts +++ b/packages/engine/src/scrollbar/index.ts @@ -1,5 +1,5 @@ import { EventEmitter2 } from 'eventemitter2'; -import { throttle } from 'lodash-es'; +import { debounce, throttle } from 'lodash-es'; import { DATA_ELEMENT, UI } from '../constants'; import { NodeInterface } from '../types'; import { $ } from '../node'; @@ -37,7 +37,6 @@ class Scrollbar extends EventEmitter2 { private sHeight: number = 0; private xWidth: number = 0; private yHeight: number = 0; - private maxScrollLeft: number = 0; #observer?: MutationObserver; #reverse?: boolean; #content?: NodeInterface; @@ -120,85 +119,121 @@ class Scrollbar extends EventEmitter2 { } } - refresh = () => { + getWidth() { const element = this.container.get(); - if (element) { - const offsetWidth = this.#scroll?.getOffsetWidth - ? this.#scroll.getOffsetWidth(element.offsetWidth) - : element.offsetWidth; + if (!element) return 0; + const offsetWidth = this.#scroll?.getOffsetWidth + ? this.#scroll.getOffsetWidth(element.offsetWidth) + : element.offsetWidth; + return offsetWidth; + } - const { offsetHeight, scrollTop } = element; - const contentElement = this.#content?.get(); - const sPLeft = removeUnit(this.container.css('padding-left')); - const sPRight = removeUnit(this.container.css('padding-right')); - const sPTop = removeUnit(this.container.css('padding-top')); - const sPBottom = removeUnit(this.container.css('padding-bottom')); - const scrollWidth = contentElement - ? contentElement.offsetWidth + sPLeft + sPRight - : element.scrollWidth; - const scrollHeight = contentElement - ? contentElement.offsetHeight + sPTop + sPBottom - : element.scrollHeight; - this.oWidth = - offsetWidth - - removeUnit(this.container.css('border-left-width')) - - removeUnit(this.container.css('border-right-width')); - this.oHeight = - offsetHeight - - removeUnit(this.container.css('border-top-width')) - - removeUnit(this.container.css('border-bottom-width')); - this.sWidth = scrollWidth; - this.sHeight = scrollHeight; - this.xWidth = Math.floor((this.oWidth * this.oWidth) / scrollWidth); - this.yHeight = Math.floor( - (this.oHeight * this.oHeight) / scrollHeight, - ); - this.maxScrollLeft = scrollWidth - this.oWidth; - if (this.x) { - this.slideX?.css('width', this.xWidth + 'px'); - const display = - this.oWidth - sPLeft - sPRight === this.sWidth || - (contentElement && - contentElement.offsetWidth <= - this.oWidth - sPLeft - sPRight) - ? 'none' - : 'block'; - this.slideX?.css('display', display); - this.emit('display', display); - this.shadowLeft?.css('display', display); - this.shadowRight?.css('display', display); - } - if (this.y) { - this.slideY?.css('height', this.yHeight + 'px'); - const display = - this.oHeight - sPTop - sPBottom === this.sHeight || - (contentElement && - contentElement.offsetHeight <= - this.oHeight - sPTop - sPBottom) - ? 'none' - : 'block'; - this.slideY?.css('display', display); - this.emit('display', display); - } - // 实际内容宽度小于容器滚动宽度(有内容删除了) - if ( - this.x && - contentElement && - element.scrollWidth - sPLeft - sPRight > - contentElement.offsetWidth - ) { - let left = - element.scrollWidth - - sPLeft - - sPRight - - contentElement.offsetWidth; + refresh = debounce( + () => { + const element = this.container.get(); + if (element) { + const { offsetHeight, scrollTop } = element; + const contentElement = this.#content?.get(); + const sPLeft = removeUnit(this.container.css('padding-left')); + const sPRight = removeUnit(this.container.css('padding-right')); + const sPTop = removeUnit(this.container.css('padding-top')); + const sPBottom = removeUnit( + this.container.css('padding-bottom'), + ); + const scrollWidth = contentElement + ? contentElement.offsetWidth + sPLeft + sPRight + : element.scrollWidth; + const scrollHeight = contentElement + ? contentElement.offsetHeight + sPTop + sPBottom + : element.scrollHeight; + this.oWidth = this.getWidth(); + this.oHeight = + offsetHeight - + removeUnit(this.container.css('border-top-width')) - + removeUnit(this.container.css('border-bottom-width')); + this.sWidth = scrollWidth; + this.sHeight = scrollHeight; + this.xWidth = Math.floor( + (this.oWidth * this.oWidth) / scrollWidth, + ); + this.yHeight = Math.floor( + (this.oHeight * this.oHeight) / scrollHeight, + ); + if (this.x) { + this.slideX?.css('width', this.xWidth + 'px'); + const display = + this.oWidth - sPLeft - sPRight === this.sWidth || + (contentElement && + contentElement.offsetWidth <= + this.oWidth - sPLeft - sPRight) + ? 'none' + : 'block'; + this.slideX?.css('display', display); + this.emit('display', display); + this.shadowLeft?.css('display', display); + this.shadowRight?.css('display', display); + } + if (this.y) { + this.slideY?.css('height', this.yHeight + 'px'); + const display = + this.oHeight - sPTop - sPBottom === this.sHeight || + (contentElement && + contentElement.offsetHeight <= + this.oHeight - sPTop - sPBottom) + ? 'none' + : 'block'; + this.slideY?.css('display', display); + this.emit('display', display); + } + // 实际内容宽度小于容器滚动宽度(有内容删除了) + if ( + this.x && + contentElement && + element.scrollWidth - sPLeft - sPRight > + contentElement.offsetWidth + ) { + let left = + element.scrollWidth - + sPLeft - + sPRight - + contentElement.offsetWidth; + if (this.#scroll) { + const { onScrollX, getScrollLeft } = this.#scroll; + + 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; + else element.scrollLeft = 0; + } + this.scroll({ left }); + } else { + element.scrollLeft -= left; + } + return; + } + // 实际内容高度小于容器滚动高度(有内容删除了) + if ( + this.y && + contentElement && + element.scrollHeight - sPTop - sPBottom !== + contentElement.offsetHeight + ) { + element.scrollTop -= + element.scrollHeight - + sPTop - + sPBottom - + contentElement.offsetHeight; + return; + } + const left = this.#scroll?.getScrollLeft + ? this.#scroll.getScrollLeft(element.scrollLeft) + : element.scrollLeft; if (this.#scroll) { - const { onScrollX, getScrollLeft } = this.#scroll; - - left = getScrollLeft - ? getScrollLeft(-0) + element.scrollLeft - left - : element.scrollLeft - left; - if (left < 0) left = 0; + const { onScrollX } = this.#scroll; if (onScrollX) { const result = onScrollX(left); if (result > 0) element.scrollLeft = result; @@ -206,31 +241,14 @@ class Scrollbar extends EventEmitter2 { } this.scroll({ left }); } else { - element.scrollLeft -= left; + this.reRenderX(left); } - return; + this.reRenderY(scrollTop); } - // 实际内容高度小于容器滚动高度(有内容删除了) - if ( - this.y && - contentElement && - element.scrollHeight - sPTop - sPBottom !== - contentElement.offsetHeight - ) { - element.scrollTop -= - element.scrollHeight - - sPTop - - sPBottom - - contentElement.offsetHeight; - return; - } - const left = this.#scroll?.getScrollLeft - ? this.#scroll.getScrollLeft(element.scrollLeft) - : element.scrollLeft; - this.reRenderX(left); - this.reRenderY(scrollTop); - } - }; + }, + 50, + { leading: true }, + ); /** * 启用鼠标在内容节点上滚动或在移动设备使用手指滑动 @@ -591,13 +609,12 @@ class Scrollbar extends EventEmitter2 { let min = value <= 0 ? 0 : left / value; min = Math.min(1, min); this.slideX?.css('left', (this.oWidth - this.xWidth) * min + 'px'); - this.reRenderShadow(left); - if (left === removeUnit(this.scrollBarX?.css('left') || '0')) - return; this.emit('change', { x: left, y: removeUnit(this.scrollBarY?.css('top') || '0'), }); + this.oWidth = this.getWidth(); + this.reRenderShadow(left); } }; @@ -608,7 +625,6 @@ class Scrollbar extends EventEmitter2 { let min = value <= 0 ? 0 : top / value; min = Math.min(1, min); this.slideY?.css('top', (this.oHeight - this.yHeight) * min + 'px'); - if (top === removeUnit(this.scrollBarX?.css('top') || '0')) return; this.emit('change', { x: removeUnit(this.scrollBarX?.css('left') || '0'), y: top, @@ -652,6 +668,7 @@ class Scrollbar extends EventEmitter2 { } this.#observer?.disconnect(); window.removeEventListener('resize', this.refresh); + window.removeEventListener('scroll', this.refresh); } } diff --git a/packages/engine/src/typing/keydown/backspace.ts b/packages/engine/src/typing/keydown/backspace.ts index 94f3d25c..9fea694f 100644 --- a/packages/engine/src/typing/keydown/backspace.ts +++ b/packages/engine/src/typing/keydown/backspace.ts @@ -78,7 +78,14 @@ class Backspace implements TypingHandleInterface { result = listener(event); if (result === false) break; } - if (result === false) return; + if (result === false) { + if (this.engine.scrollNode) + range.scrollIntoViewIfNeeded( + this.engine.container, + this.engine.scrollNode, + ); + return; + } // 范围为展开状态 if (!range.collapsed) { event.preventDefault(); @@ -88,6 +95,11 @@ class Backspace implements TypingHandleInterface { } change.delete(range); change.apply(range); + if (this.engine.scrollNode) + range.scrollIntoViewIfNeeded( + this.engine.container, + this.engine.scrollNode, + ); return; } else { let brNode: NodeInterface | undefined = undefined; diff --git a/plugins/image/src/uploader.ts b/plugins/image/src/uploader.ts index f8b5b56b..50535671 100644 --- a/plugins/image/src/uploader.ts +++ b/plugins/image/src/uploader.ts @@ -685,13 +685,13 @@ export default class extends Plugin { if (!text) return; // 带跳转链接的图片 let reg = - /(\[!\[([^\]]{0,})\]\((https?:\/\/[^\)]{5,})\)\]\(([\S]+?)\))|(!\[([^\]]{0,})\]\((https?:\/\/[^\)]{5,})\))/; + /(\[!\[([^\]]{0,})\]\((\S{0,})(https?:\/\/[^\)]{5,})\)\]\(([\S]+?)\))|(!\[([^\]]{0,})\]\((https?:\/\/[^\)]{5,})\))/i; let match = reg.exec(text); if (!match) { // 无跳转链接的图片 //![aomao-preview](https://user-images.githubusercontent.com/55792257/125074830-62d79300-e0f0-11eb-8d0f-bb96a7775568.png) - reg = /(!\[([^\]]{0,})\]\((https?:\/\/[^\)]{5,})\))/; + reg = /(!\[([^\]]{0,})\]\((\S{0,})(https?:\/\/[^\)]{5,})\))/i; match = reg.exec(text); } return { @@ -720,9 +720,9 @@ export default class extends Plugin { //从匹配结束位置分割 textNode = regNode.splitText(match[0].length); const isLink = match[0].startsWith('['); - const alt = isLink ? match[2] : match[6]; - const src = isLink ? match[3] : match[7]; - const link = isLink ? match[4] : ''; + const alt = match[2] || match[6]; + const src = match[4] || match[8]; + const link = isLink ? match[5] : ''; const cardNode = card.replaceNode($(regNode), 'image', { src, diff --git a/plugins/table/src/index.css b/plugins/table/src/index.css index 79a11111..c5f36e7c 100644 --- a/plugins/table/src/index.css +++ b/plugins/table/src/index.css @@ -18,6 +18,14 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"] background: transparent } +.am-engine [data-card-key="table"].card-selected [data-card-element="center"].data-card-background-selected { + background: transparent; +} + +.am-engine [data-card-key="table"].card-selected [data-card-element="center"].data-card-background-selected .table-wrapper { + background: rgba(27, 162, 227, 0.2); +} + .am-engine-mobile div[data-card-key="table"].card-activated { margin-left: 20px; }