UNPKG

@textbus/platform-browser

Version:

Textbus is a rich text editor and framework that is highly customizable and extensible to achieve rich wysiwyg effects.

1,372 lines (1,354 loc) 105 kB
'use strict'; require('reflect-metadata'); var core$1 = require('@textbus/core'); var stream = require('@tanbo/stream'); var core = require('@viewfly/core'); function createElement(tagName, options = {}) { const el = document.createElement(tagName); if (options.classes) { el.classList.add(...options.classes); } if (options.attrs) { Object.keys(options.attrs).forEach(key => { el.setAttribute(key, options.attrs[key]); }); } if (options.props) { Object.keys(options.props).forEach(key => { el[key] = options.props[key]; }); } if (options.styles) { Object.assign(el.style, options.styles); } if (options.children) { options.children.filter(i => i).forEach(item => { el.appendChild(item); }); } if (options.on) { Object.keys(options.on).forEach(key => { el.addEventListener(key, options.on[key]); }); } return el; } function getLayoutRectByRange(range) { let { startContainer, startOffset } = range; if (startContainer.nodeType === Node.TEXT_NODE) { if (startOffset > 0) { return range.getBoundingClientRect(); } const parentNode = startContainer.parentNode; startOffset = Array.from(parentNode.childNodes).indexOf(startContainer); startContainer = parentNode; } const beforeNode = startContainer.childNodes[startOffset - 1]; if (beforeNode) { if (beforeNode.nodeType === Node.ELEMENT_NODE && beforeNode.nodeName.toLowerCase() !== 'br') { const rect = beforeNode.getBoundingClientRect(); return { left: rect.right, top: rect.top, width: range.collapsed ? 0 : rect.width, height: rect.height }; } else if (beforeNode.nodeType === Node.TEXT_NODE) { const range2 = document.createRange(); range2.setStart(beforeNode, beforeNode.textContent.length); range2.setEnd(beforeNode, beforeNode.textContent.length); const rect = range2.getBoundingClientRect(); return { left: rect.right, top: rect.top, width: range.collapsed ? 0 : rect.width, height: rect.height }; } } const offsetNode = startContainer.childNodes[startOffset]; let isInsertBefore = false; if (!offsetNode) { const lastChild = startContainer.lastChild; if (lastChild && lastChild.nodeType === Node.ELEMENT_NODE) { const rect = lastChild.getBoundingClientRect(); return { left: rect.right, top: rect.top, width: range.collapsed ? 0 : rect.width, height: rect.height }; } } if (offsetNode) { if (offsetNode.nodeType === Node.ELEMENT_NODE && offsetNode.nodeName.toLowerCase() !== 'br') { const rect = offsetNode.getBoundingClientRect(); return { left: rect.left, top: rect.top, width: range.collapsed ? 0 : rect.width, height: rect.height }; } isInsertBefore = true; } const span = startContainer.ownerDocument.createElement('span'); span.innerText = '\u200b'; span.style.display = 'inline-block'; if (isInsertBefore) { startContainer.insertBefore(span, offsetNode); } else { startContainer.appendChild(span); } const rect = span.getBoundingClientRect(); startContainer.removeChild(span); return { left: rect.left, top: rect.top, width: range.collapsed ? 0 : rect.width, height: rect.height }; } const isWindows = () => /win(dows|32|64)/i.test(navigator.userAgent); const isMac = () => /mac os/i.test(navigator.userAgent); const isSafari = () => /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); const isFirefox = () => /Firefox/.test(navigator.userAgent); const isMobileBrowser = () => /Android|iPhone|iPad/.test(navigator.userAgent); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol */ function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function __param(paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } } function __metadata(metadataKey, metadataValue) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * 编辑器可选项依赖注入 token */ const EDITOR_OPTIONS = new core.InjectionToken('EDITOR_OPTIONS'); /** * 编辑器容器依赖注入 token */ const VIEW_CONTAINER = new core.InjectionToken('VIEW_CONTAINER'); /** * 编辑器容器依赖注入 token */ const VIEW_DOCUMENT = new core.InjectionToken('VIEW_DOCUMENT'); /** * 编辑器容器遮罩层 token */ const VIEW_MASK = new core.InjectionToken('VIEW_MASK'); var Parser_1; /** * 用于解析 HTML,并把 HTML 内容转换为 Textbus 可以支持的组件或插槽数据 */ exports.Parser = Parser_1 = class Parser { static parseHTML(html) { return new DOMParser().parseFromString(html, 'text/html').body; } constructor(options, textbus) { Object.defineProperty(this, "textbus", { enumerable: true, configurable: true, writable: true, value: textbus }); Object.defineProperty(this, "componentLoaders", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "formatLoaders", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "attributeLoaders", { enumerable: true, configurable: true, writable: true, value: void 0 }); const componentLoaders = [ ...(options.componentLoaders || []) ]; const formatLoaders = [ ...(options.formatLoaders || []) ]; const attributeLoaders = [ ...(options.attributeLoaders || []) ]; // options.imports?.forEach(i => { // componentLoaders.push(...(i.componentLoaders || [])) // formatLoaders.push(...(i.formatLoaders || [])) // }) this.componentLoaders = componentLoaders; this.formatLoaders = formatLoaders; this.attributeLoaders = attributeLoaders; } /** * 使用指定的组件加载器解析一段 HTML 字符串或 DOM 元素 * @param html * @param rootComponentLoader */ parseDoc(html, rootComponentLoader) { const element = typeof html === 'string' ? Parser_1.parseHTML(html) : html; return rootComponentLoader.read(element, this.textbus, (childSlot, slotRootElement, slotContentHostElement = slotRootElement) => { return this.readSlot(childSlot, slotRootElement, slotContentHostElement); }); } /** * 将一段 HTML 或 DOM 元素解析到指定插槽 * @param html * @param rootSlot */ parse(html, rootSlot) { const element = typeof html === 'string' ? Parser_1.parseHTML(html) : html; return this.readFormats(element, rootSlot); } readComponent(el, slot) { if (el.nodeType === Node.ELEMENT_NODE) { if (el.tagName === 'BR') { slot.insert('\n'); return; } const schema = [...slot.schema]; for (const t of this.componentLoaders) { if (t.match(el, schema)) { const result = t.read(el, this.textbus, (childSlot, slotRootElement, slotContentHostElement = slotRootElement) => { return this.readSlot(childSlot, slotRootElement, slotContentHostElement); }); if (!result) { return; } if (result instanceof core$1.Slot) { result.toDelta().forEach(i => slot.insert(i.insert, i.formats)); return; } slot.insert(result); return; } } this.readFormats(el, slot); } else if (el.nodeType === Node.TEXT_NODE) { this.readText(slot, el); } } readText(slot, el) { const textContent = el.textContent; if (/^\s*[\r\n\u200b]+\s*$/.test(textContent)) { return; } slot.insert(textContent); } readFormats(el, slot) { const formats = this.formatLoaders.filter(f => { return f.match(el); }).map(f => { return f.read(el); }); const startIndex = slot.index; let startNode = el.firstChild; while (startNode) { this.readComponent(startNode, slot); startNode = startNode.nextSibling; } const endIndex = slot.index; this.applyFormats(slot, formats.map(i => { return { formatter: i.formatter, value: i.value, startIndex, endIndex }; })); slot.retain(endIndex); return slot; } readSlot(childSlot, slotRootElement, slotContentElement) { if (slotRootElement.nodeType === Node.ELEMENT_NODE) { this.attributeLoaders.filter(a => { return a.match(slotRootElement); }).forEach(a => { const r = a.read(slotRootElement); childSlot.setAttribute(r.attribute, r.value); }); } if (slotContentElement.nodeType === Node.ELEMENT_NODE) { this.readFormats(slotContentElement, childSlot); } else { this.readText(childSlot, slotContentElement); } return childSlot; } applyFormats(slot, formatItems) { slot.background(() => { formatItems.forEach(i => { slot.retain(i.startIndex); slot.retain(i.endIndex - i.startIndex, i.formatter, i.value); }); }); } }; exports.Parser = Parser_1 = __decorate([ core.Injectable(), __param(0, core.Inject(EDITOR_OPTIONS)), __metadata("design:paramtypes", [Object, core$1.Textbus]) ], exports.Parser); class Input { } class DomAdapter extends core$1.Adapter { constructor() { super(...arguments); Object.defineProperty(this, "onViewUpdated", { enumerable: true, configurable: true, writable: true, value: new stream.Subject() }); Object.defineProperty(this, "host", { enumerable: true, configurable: true, writable: true, value: createElement('div', { styles: { cursor: 'text', wordBreak: 'break-all', boxSizing: 'border-box', flex: 1, outline: 'none' }, attrs: { 'data-textbus-view': VIEW_DOCUMENT, }, props: { id: 'textbus-' + Number((Math.random() + '').substring(2)).toString(16) } }) }); } } /** * Textbus PC 端选区桥接实现 */ exports.SelectionBridge = class SelectionBridge { constructor(config, textbus, controller, selection, rootComponentRef, input, scheduler, domAdapter) { Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: config }); Object.defineProperty(this, "selection", { enumerable: true, configurable: true, writable: true, value: selection }); Object.defineProperty(this, "rootComponentRef", { enumerable: true, configurable: true, writable: true, value: rootComponentRef }); Object.defineProperty(this, "input", { enumerable: true, configurable: true, writable: true, value: input }); Object.defineProperty(this, "scheduler", { enumerable: true, configurable: true, writable: true, value: scheduler }); Object.defineProperty(this, "domAdapter", { enumerable: true, configurable: true, writable: true, value: domAdapter }); Object.defineProperty(this, "onSelectionChange", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "nativeSelection", { enumerable: true, configurable: true, writable: true, value: document.getSelection() }); Object.defineProperty(this, "syncSelectionFromNativeSelectionChange", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "selectionChangeEvent", { enumerable: true, configurable: true, writable: true, value: new stream.Subject() }); Object.defineProperty(this, "subs", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "sub", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "connector", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "ignoreSelectionChange", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "changeFromUser", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "docContainer", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "cacheCaretPositionTimer", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "oldCaretPosition", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.docContainer = textbus.get(VIEW_DOCUMENT); this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(stream.filter(() => { return !controller.readonly; })); this.sub = this.onSelectionChange.subscribe((r) => { if (r) { input.focus(r, this.changeFromUser); } else { input.blur(); } }); this.sub.add(stream.fromEvent(document, 'focusin').subscribe(ev => { let target = ev.target; if (/^(input|textarea|select)$/i.test(target.nodeName)) { if (target.tagName.toLowerCase() === 'input' && /^(range|date)$/.test(target.type)) { return; } this.ignoreSelectionChange = true; return; } if (!config.useContentEditable) { while (target) { if (target.contentEditable === 'true') { this.ignoreSelectionChange = true; return; } target = target.parentNode; } } })); this.sub.add(stream.fromEvent(document, 'focusout').subscribe(() => { this.ignoreSelectionChange = false; })); } connect(connector) { this.disConnect(); this.connector = connector; this.syncSelection(connector); this.listen(connector); } disConnect() { this.connector = null; this.unListen(); } getRect(location) { const { focus, anchor } = this.getPositionByRange({ focusOffset: location.offset, anchorOffset: location.offset, focusSlot: location.slot, anchorSlot: location.slot }); if (!focus || !anchor) { return null; } const nativeRange = document.createRange(); nativeRange.setStart(focus.node, focus.offset); nativeRange.collapse(); return getLayoutRectByRange(nativeRange); } restore(abstractSelection, formLocal) { this.changeFromUser = formLocal; if (this.ignoreSelectionChange || !this.connector) { return; } this.unListen(); if (!abstractSelection) { this.nativeSelection.removeAllRanges(); this.selectionChangeEvent.next(null); this.listen(this.connector); return; } const { focus, anchor } = this.getPositionByRange(abstractSelection); if (!focus || !anchor) { this.nativeSelection.removeAllRanges(); this.selectionChangeEvent.next(null); this.listen(this.connector); return; } function tryOffset(position) { if (!position.node) { return; } if (position.node.nodeType === Node.TEXT_NODE) { const len = position.node.textContent.length; if (position.offset > len) { position.offset = len; } } else if (position.node.nodeType === Node.ELEMENT_NODE) { const len = position.node.childNodes.length; if (position.offset > len) { position.offset = len; } } } try { tryOffset(focus); tryOffset(anchor); this.nativeSelection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset); } catch (e) { setTimeout(() => { throw e; }); } if (this.nativeSelection.rangeCount) { const nativeRange = this.nativeSelection.getRangeAt(0); this.selectionChangeEvent.next(nativeRange); } else { this.selectionChangeEvent.next(null); } // hack start 浏览器会触发上面选区更改事件 const bind = () => { if (this.connector) { this.listen(this.connector); } }; if (typeof requestIdleCallback === 'function') { requestIdleCallback(bind); } else { setTimeout(bind, 30); } // hack end } destroy() { this.subs.forEach(i => i.unsubscribe()); this.sub.unsubscribe(); } getPositionByRange(abstractSelection) { let focus; let anchor; try { focus = this.findSelectedNodeAndOffset(abstractSelection.focusSlot, abstractSelection.focusOffset); anchor = focus; if (abstractSelection.anchorSlot !== abstractSelection.focusSlot || abstractSelection.anchorOffset !== abstractSelection.focusOffset) { anchor = this.findSelectedNodeAndOffset(abstractSelection.anchorSlot, abstractSelection.anchorOffset); } return { focus, anchor }; } catch (e) { return { focus: null, anchor: null }; } } getPreviousLinePositionByCurrent(position) { return this.getLinePosition(position, false); } getNextLinePositionByCurrent(position) { return this.getLinePosition(position, true); } getLinePosition(currentPosition, toNext) { clearTimeout(this.cacheCaretPositionTimer); let p; if (this.oldCaretPosition) { p = toNext ? this.getNextLinePositionByOffset(currentPosition, this.oldCaretPosition.left) : this.getPreviousLinePositionByOffset(currentPosition, this.oldCaretPosition.left); } else { this.oldCaretPosition = this.getRect(currentPosition); p = toNext ? this.getNextLinePositionByOffset(currentPosition, this.oldCaretPosition.left) : this.getPreviousLinePositionByOffset(currentPosition, this.oldCaretPosition.left); } this.cacheCaretPositionTimer = setTimeout(() => { this.oldCaretPosition = null; }, 3000); return p; } /** * 获取选区向上移动一行的位置。 * @param currentPosition * @param startLeft 参考位置。 */ getPreviousLinePositionByOffset(currentPosition, startLeft) { let isToPrevLine = false; let loopCount = 0; let minLeft = startLeft; let focusSlot = currentPosition.slot; let focusOffset = currentPosition.offset; let minTop = this.getRect({ slot: focusSlot, offset: focusOffset }).top; let position; let oldPosition; let oldLeft = 0; while (true) { loopCount++; position = this.selection.getPreviousPositionByPosition(focusSlot, focusOffset); focusSlot = position.slot; focusOffset = position.offset; const rect2 = this.getRect(position); if (!isToPrevLine) { if (rect2.left > minLeft || rect2.top + rect2.height <= minTop) { isToPrevLine = true; } else if (rect2.left === minLeft && rect2.top === minTop) { return position; } minLeft = rect2.left; minTop = rect2.top; // oldPosition = position } if (isToPrevLine) { if (rect2.left <= startLeft) { return position; } if (oldPosition) { if (rect2.left >= oldLeft) { return oldPosition; } } oldLeft = rect2.left; oldPosition = position; } if (loopCount > 10000) { break; } } return position || { offset: 0, slot: focusSlot }; } /** * 获取选区向下移动一行的位置。 * @param currentPosition * @param startLeft 参考位置。 */ getNextLinePositionByOffset(currentPosition, startLeft) { let isToNextLine = false; let loopCount = 0; let maxRight = startLeft; let focusSlot = currentPosition.slot; let focusOffset = currentPosition.offset; const rect = this.getRect({ slot: focusSlot, offset: focusOffset }); let minTop = rect.top; let oldPosition; let oldLeft = 0; while (true) { loopCount++; const position = this.selection.getNextPositionByPosition(focusSlot, focusOffset); focusSlot = position.slot; focusOffset = position.offset; const rect2 = this.getRect(position); if (!isToNextLine) { if (rect2.left < maxRight || rect2.top >= minTop + rect.height) { isToNextLine = true; } else if (rect2.left === maxRight && rect2.top === minTop) { return position; } maxRight = rect2.left; minTop = rect2.top; oldPosition = position; } if (isToNextLine) { if (rect2.left > startLeft) { return oldPosition; } if (oldPosition) { if (rect2.left <= oldLeft) { return oldPosition; } } oldPosition = position; oldLeft = rect2.left; } if (loopCount > 10000) { break; } } return oldPosition || { offset: focusSlot.length, slot: focusSlot }; } unListen() { this.subs.forEach(i => i.unsubscribe()); this.subs = []; } listen(connector) { if (!this.config.useContentEditable) { const selection = this.nativeSelection; this.subs.push(stream.fromEvent(this.docContainer, 'mousedown').subscribe(ev => { if (this.ignoreSelectionChange || ev.button === 2) { return; } if (!ev.shiftKey) { selection.removeAllRanges(); } })); } let isUpdating = false; this.subs.push(this.scheduler.onDocChange.subscribe(() => { isUpdating = true; }), this.scheduler.onDocChanged.pipe(stream.delay()).subscribe(() => { isUpdating = false; }), stream.fromEvent(document, 'selectionchange').pipe().subscribe(() => { if (isUpdating) { return; } if (this.syncSelectionFromNativeSelectionChange) { this.syncSelection(connector); } })); } syncSelection(connector) { var _a; const selection = this.nativeSelection; this.changeFromUser = true; if (this.ignoreSelectionChange || this.input.composition || selection.rangeCount === 0 || !this.docContainer.contains(selection.anchorNode) || this.rootComponentRef.component.slots.length === 0) { return; } const rawRange = selection.getRangeAt(0); const nativeRange = rawRange.cloneRange(); const isFocusEnd = selection.focusNode === nativeRange.endContainer && selection.focusOffset === nativeRange.endOffset; const isFocusStart = selection.focusNode === nativeRange.startContainer && selection.focusOffset === nativeRange.startOffset; if (!this.docContainer.contains(selection.focusNode)) { if (isFocusEnd) { const nativeNode = this.domAdapter.getNativeNodeBySlot(this.rootComponentRef.component.slots.at(0)); if (!nativeNode) { return; } nativeRange.setEndAfter(nativeNode.lastChild); } else { const nativeNode = this.domAdapter.getNativeNodeBySlot(this.rootComponentRef.component.slots.at(-1)); if (!nativeNode) { return; } nativeRange.setStartBefore(nativeNode.firstChild); } } const startPosition = this.getCorrectedPosition(nativeRange.startContainer, nativeRange.startOffset, isFocusStart); const endPosition = nativeRange.collapsed ? startPosition : this.getCorrectedPosition(nativeRange.endContainer, nativeRange.endOffset, isFocusEnd); if ([Node.ELEMENT_NODE, Node.TEXT_NODE].includes((_a = nativeRange.commonAncestorContainer) === null || _a === void 0 ? void 0 : _a.nodeType) && startPosition && endPosition) { const abstractSelection = isFocusEnd ? { anchorSlot: startPosition.slot, anchorOffset: startPosition.offset, focusSlot: endPosition.slot, focusOffset: endPosition.offset } : { focusSlot: startPosition.slot, focusOffset: startPosition.offset, anchorSlot: endPosition.slot, anchorOffset: endPosition.offset }; const { focus, anchor } = this.getPositionByRange(abstractSelection); if (focus && anchor) { let start = anchor; let end = focus; if (isFocusStart) { start = focus; end = anchor; } if (nativeRange.startContainer !== start.node || nativeRange.startOffset !== start.offset) { nativeRange.setStart(start.node, start.offset); } if (nativeRange.endContainer !== end.node || nativeRange.endOffset !== end.offset) { nativeRange.setEnd(end.node, end.offset); } connector.setSelection(abstractSelection); if (selection.isCollapsed && (rawRange.startContainer !== start.node || rawRange.startOffset !== start.offset || rawRange.endContainer !== end.node || rawRange.endOffset !== end.offset)) { rawRange.setStart(start.node, start.offset); rawRange.setEnd(end.node, end.offset); } this.selectionChangeEvent.next(nativeRange); } else { connector.setSelection(null); } return; } connector.setSelection(null); } findSelectedNodeAndOffset(slot, offset) { const prev = slot.getContentAtIndex(offset - 1); const nodes = this.domAdapter.getNodesBySlot(slot); if (prev) { if (typeof prev !== 'string') { const nativeNode = this.domAdapter.getNativeNodeByComponent(prev); return { node: nativeNode.parentNode, offset: Array.from(nativeNode.parentNode.childNodes).indexOf(nativeNode) + 1 }; } else if (prev === '\n') { for (const node of nodes) { if (node instanceof Text) { continue; } if (node.nodeName === 'BR') { const position = this.domAdapter.getLocationByNativeNode(node); if (position) { if (position.endIndex === offset) { const parentNode = node.parentNode; return { node: parentNode, offset: Array.from(parentNode.childNodes).indexOf(node) + 1 }; } } } } } } const current = slot.getContentAtIndex(offset); if (current && typeof current !== 'string') { const nativeNode = this.domAdapter.getNativeNodeByComponent(current); return { node: nativeNode.parentNode, offset: Array.from(nativeNode.parentNode.childNodes).indexOf(nativeNode) }; } for (const node of nodes) { if (node instanceof Element) { if (node.tagName === 'BR') { const position = this.domAdapter.getLocationByNativeNode(node); if (position) { if (position.startIndex === offset) { const parentNode = node.parentNode; return { node: parentNode, offset: Array.from(parentNode.childNodes).indexOf(node) }; } } } continue; } const position = this.domAdapter.getLocationByNativeNode(node); if (position) { if (offset >= position.startIndex && offset <= position.endIndex) { return { node: node, offset: offset - position.startIndex }; } } } return null; } getCorrectedPosition(node, offset, toAfter, excludeNodes = []) { excludeNodes.push(node); if (node.nodeType === Node.ELEMENT_NODE) { const containerPosition = this.domAdapter.getLocationByNativeNode(node); const childNode = node.childNodes[offset]; if (childNode) { const childPosition = this.domAdapter.getLocationByNativeNode(childNode); if (childPosition) { if (containerPosition) { return { slot: childPosition.slot, offset: childPosition.startIndex }; } return this.findFocusNode(childNode, toAfter, excludeNodes); } return this.findFocusNode(childNode, toAfter, excludeNodes); } const prevNode = node.childNodes[offset - 1]; if (prevNode) { const prevPosition = this.domAdapter.getLocationByNativeNode(prevNode); if (prevPosition && containerPosition) { return { slot: prevPosition.slot, offset: prevPosition.endIndex }; } } if (containerPosition) { return { slot: containerPosition.slot, offset: containerPosition.endIndex }; } const nextNode = toAfter ? node.nextSibling : node.previousSibling; if (nextNode) { return this.findFocusNode(nextNode, toAfter, excludeNodes); } return this.findFocusNodeByParent(node, toAfter, excludeNodes); } else if (node.nodeType === Node.TEXT_NODE) { const containerPosition = this.domAdapter.getLocationByNativeNode(node); if (containerPosition) { return { slot: containerPosition.slot, offset: containerPosition.startIndex + offset }; } const nextNode = toAfter ? node.nextSibling : node.previousSibling; if (nextNode) { return this.findFocusNode(nextNode, toAfter, excludeNodes); } return this.findFocusNodeByParent(node, toAfter, excludeNodes); } return null; } findFocusNode(node, toAfter = false, excludeNodes = []) { if (excludeNodes.includes(node)) { const next = toAfter ? node.nextSibling : node.previousSibling; if (next) { return this.findFocusNode(next, toAfter, excludeNodes); } return this.findFocusNodeByParent(node, toAfter, excludeNodes); } excludeNodes.push(node); const position = this.domAdapter.getLocationByNativeNode(node); if (position) { return { slot: position.slot, offset: toAfter ? position.startIndex : position.endIndex }; } const firstChild = toAfter ? node.firstChild : node.lastChild; if (firstChild) { return this.findFocusNode(firstChild, toAfter, excludeNodes); } const nextSibling = toAfter ? node.nextSibling : node.previousSibling; if (nextSibling) { return this.findFocusNode(nextSibling, toAfter, excludeNodes); } return this.findFocusNodeByParent(node, toAfter, excludeNodes); } findFocusNodeByParent(node, toAfter, excludeNodes) { const parentNode = node.parentNode; if (parentNode) { const parentPosition = this.domAdapter.getLocationByNativeNode(parentNode); if (parentPosition) { return { slot: parentPosition.slot, offset: toAfter ? parentPosition.endIndex : parentPosition.startIndex }; } excludeNodes.push(node); return this.findFocusNode(parentNode, toAfter, excludeNodes); } return null; } }; exports.SelectionBridge = __decorate([ core.Injectable(), __param(0, core.Inject(EDITOR_OPTIONS)), __metadata("design:paramtypes", [Object, core$1.Textbus, core$1.Controller, core$1.Selection, core$1.RootComponentRef, Input, core$1.Scheduler, DomAdapter]) ], exports.SelectionBridge); const iframeHTML = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Textbus</title> <style> html {position: fixed; left:0; overflow: hidden} html, body{height: 100%;width:100%} body{margin:0; overflow: hidden} textarea{width: 2000px;height: 100%;opacity: 0; padding: 0; outline: none; border: none; position: absolute; left:0; top:0;} </style> </head> <body> </body> </html> `; class ExperimentalCaret { get rect() { return this.caret.getBoundingClientRect(); } set display(v) { this._display = v; this.caret.style.visibility = v ? 'visible' : 'hidden'; } get display() { return this._display; } constructor(domRenderer, scheduler, editorMask) { Object.defineProperty(this, "domRenderer", { enumerable: true, configurable: true, writable: true, value: domRenderer }); Object.defineProperty(this, "scheduler", { enumerable: true, configurable: true, writable: true, value: scheduler }); Object.defineProperty(this, "editorMask", { enumerable: true, configurable: true, writable: true, value: editorMask }); Object.defineProperty(this, "onPositionChange", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "onStyleChange", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "elementRef", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "changeFromSelf", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "getLimit", { enumerable: true, configurable: true, writable: true, value: function () { return { top: 0, bottom: document.documentElement.clientHeight }; } }); Object.defineProperty(this, "timer", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "caret", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_display", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "flashing", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "subscription", { enumerable: true, configurable: true, writable: true, value: new stream.Subscription() }); Object.defineProperty(this, "positionChangeEvent", { enumerable: true, configurable: true, writable: true, value: new stream.Subject() }); Object.defineProperty(this, "styleChangeEvent", { enumerable: true, configurable: true, writable: true, value: new stream.Subject() }); Object.defineProperty(this, "oldRange", { enumerable: true, configurable: true, writable: true, value: null }); this.onPositionChange = this.positionChangeEvent.pipe(stream.distinctUntilChanged()); this.onStyleChange = this.styleChangeEvent.asObservable(); this.elementRef = createElement('div', { styles: { position: 'absolute', width: '2px', pointerEvents: 'none' }, children: [ this.caret = createElement('span', { styles: { width: '100%', height: '100%', position: 'absolute', left: 0, top: 0 } }) ] }); this.subscription.add(stream.fromEvent(document, 'mousedown').subscribe(() => { this.flashing = false; }), stream.fromEvent(document, 'mouseup').subscribe(() => { this.flashing = true; })); this.editorMask.appendChild(this.elementRef); } refresh() { if (this.oldRange) { this.show(this.oldRange, false); } } show(range, restart) { this.oldRange = range; if (restart || this.scheduler.lastChangesHasLocalUpdate) { clearTimeout(this.timer); } this.updateCursorPosition(range); if (range.collapsed) { if (restart || this.scheduler.lastChangesHasLocalUpdate) { this.display = true; const toggleShowHide = () => { this.display = !this.display || !this.flashing; this.timer = setTimeout(toggleShowHide, 400); }; clearTimeout(this.timer); this.timer = setTimeout(toggleShowHide, 400); } } else { this.display = false; clearTimeout(this.timer); } } hide() { this.display = false; clearTimeout(this.timer); this.positionChangeEvent.next(null); } destroy() { clearTimeout(this.timer); // this.caret. this.subscription.unsubscribe(); } updateCursorPosition(nativeRange) { const startContainer = nativeRange.startContainer; const node = (startContainer.nodeType === Node.ELEMENT_NODE ? startContainer : startContainer.parentNode); if ((node === null || node === void 0 ? void 0 : node.nodeType) !== Node.ELEMENT_NODE) { this.positionChangeEvent.next(null); return; } const compositionNode = this.domRenderer.compositionNode; if (compositionNode) { nativeRange = nativeRange.cloneRange(); nativeRange.selectNodeContents(compositionNode); nativeRange.collapse(); } const rect = getLayoutRectByRange(nativeRange); const { fontSize, lineHeight, color, writingMode } = getComputedStyle(node); let height; if (isNaN(+lineHeight)) { const f = parseFloat(lineHeight); if (isNaN(f)) { height = parseFloat(fontSize); } else { height = f; } } else { height = parseFloat(fontSize) * parseFloat(lineHeight); } const boxHeight = Math.max(Math.floor(Math.max(height, rect.height)), 12); // const boxHeight = Math.floor(height) let rectTop = rect.top; if (rect.height < height) { rectTop -= (height - rect.height) / 2; } rectTop = Math.floor(rectTop); const containerRect = this.editorMask.getBoundingClientRect(); const top = Math.floor(rectTop - containerRect.top); const left = Math.floor(rect.left + rect.width / 2 - containerRect.left); let rotate = 0; if (nativeRange.collapsed) { rotate = Math.round(Math.atan2(rect.width, rect.height) * 180 / Math.PI); if (rotate !== 0) { const hackEle = document.createElement('span'); // eslint-disable-next-line max-len hackEle.style.cssText = 'display: inline-block; width: 10px; height: 10px; position: relative; contain: layout style size; writing-mode: inherit'; const pointEle = document.createElement('span'); pointEle.style.cssText = 'position: absolute; left: 0; top: 0; width:0;height:0'; hackEle.append(pointEle); node.append(hackEle); const t1 = pointEle.getBoundingClientRect().top; pointEle.style.right = '0'; pointEle.style.left = ''; const t2 = pointEle.getBoundingClientRect().top; if (t2 < t1) { rotate = -rotate; } hackEle.remove(); } } if (rotate === 0 && (writingMode === 'vertical-lr' || writingMode === 'vertical-rl')) { rotate += 90; } Object.assign(this.elementRef.style, { left: left + 'px', top: top + 'px', height: boxHeight + 'px', lineHeight: boxHeight + 'px', fontSize, transform: `rotate(${rotate}deg)`, }); this.caret.style.backgroundColor = color; this.styleChangeEvent.next({ height: boxHeight + 'px', lineHeight: boxHeight + 'px', fontSize }); this.positionChangeEvent.next({ left, top: rectTop, height: boxHeight }); if (this.changeFromSelf) { this.changeFromSelf = false; const selfRect = this.elementRef.getBoundingClientRect(); const scrollContainer = this.getScrollContainer(startContainer); const scrollRect = scrollContainer === document.documentElement ? { top: 0, bottom: document.documentElement.clientHeight } : scrollContainer.getBoundingClientRect(); const limit = this.getLimit(); const top = Math.max(limit.top, scrollRect.top); const bottom = Math.min(limit.bottom, scrollRect.bottom); if (selfRect.top < top) { scrollContainer.scrollTop -= top - selfRect.top; } else if (selfRect.bottom > bottom) { scrollContainer.scrollTop += selfRect.bottom - bottom; } } } getScrollContainer(container) { while (container) { if (container instanceof Element) { const styles = getComputedStyle(container); if (styles.o