@textbus/browser
Version:
Textbus is a rich text editor and framework that is highly customizable and extensible to achieve rich wysiwyg effects.
1,345 lines (1,330 loc) • 72.1 kB
JavaScript
import 'reflect-metadata';
import { Subject, distinctUntilChanged, fromEvent, Subscription, filter, merge, map, Observable } from '@tanbo/stream';
import { InjectionToken, Injectable, Injector, Inject } from '@tanbo/di';
import { Scheduler, Slot, ContentType, Keyboard, Commander, Selection, Controller, VTextNode, VElement, RootComponentRef, Renderer, makeError, Starter, OutputRenderer, Translator, invokeListener, History, NativeRenderer, NativeSelectionBridge } from '@textbus/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 createTextNode(content) {
return document.createTextNode(content);
}
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);
/******************************************************************************
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.
***************************************************************************** */
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());
});
}
/**
* 编辑器可选项依赖注入 token
*/
const EDITOR_OPTIONS = new InjectionToken('EDITOR_OPTIONS');
/**
* 编辑器容器依赖注入 token
*/
const VIEW_CONTAINER = new InjectionToken('VIEW_CONTAINER');
/**
* 编辑器容器依赖注入 token
*/
const VIEW_DOCUMENT = new InjectionToken('VIEW_DOCUMENT');
/**
* 编辑器容器遮罩层 token
*/
const VIEW_MASK = new InjectionToken('VIEW_MASK');
function getLayoutRectByRange(range) {
const { startContainer, startOffset } = range;
if (startContainer.nodeType === Node.ELEMENT_NODE) {
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: rect.width,
height: rect.height
};
}
}
if (offsetNode) {
if (offsetNode.nodeType === Node.ELEMENT_NODE && offsetNode.nodeName.toLowerCase() !== 'br') {
return offsetNode.getBoundingClientRect();
}
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 rect;
}
return range.getBoundingClientRect();
}
let Caret = class Caret {
set display(v) {
this._display = v;
this.caret.style.visibility = v ? 'visible' : 'hidden';
}
get display() {
return this._display;
}
constructor(scheduler, injector) {
this.scheduler = scheduler;
this.injector = injector;
this.timer = null;
this.oldPosition = null;
this._display = true;
this.flashing = true;
this.subs = [];
this.positionChangeEvent = new Subject();
this.styleChangeEvent = new Subject();
this.oldRange = null;
this.isFixed = false;
this.editorMask = injector.get(VIEW_MASK);
this.onPositionChange = this.positionChangeEvent.pipe(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.subs.push(fromEvent(document, 'mousedown').subscribe(() => {
this.flashing = false;
}), fromEvent(document, 'mouseup').subscribe(() => {
this.flashing = true;
}));
this.editorMask.appendChild(this.elementRef);
}
refresh(isFixedCaret = false) {
this.isFixed = isFixedCaret;
if (this.oldRange) {
this.show(this.oldRange, false);
}
this.isFixed = false;
}
show(range, restart) {
const oldRect = this.elementRef.getBoundingClientRect();
this.oldPosition = {
top: oldRect.top,
left: oldRect.left,
height: oldRect.height
};
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.subs.forEach(i => i.unsubscribe());
}
correctScrollTop(scroller) {
const scheduler = this.scheduler;
let docIsChanged = true;
function limitPosition(position) {
const { top, bottom } = scroller.getLimit();
const caretTop = position.top;
if (caretTop + position.height > bottom) {
const offset = caretTop - bottom + position.height;
scroller.setOffset(offset);
}
else if (position.top < top) {
scroller.setOffset(-(top - position.top));
}
}
let isPressed = false;
this.subs.push(scroller.onScroll.subscribe(() => {
if (this.oldPosition) {
const rect = this.elementRef.getBoundingClientRect();
this.oldPosition.top = rect.top;
this.oldPosition.left = rect.left;
this.oldPosition.height = rect.height;
}
}), fromEvent(document, 'mousedown', true).subscribe(() => {
isPressed = true;
}), fromEvent(document, 'mouseup', true).subscribe(() => {
isPressed = false;
}), scheduler.onDocChange.subscribe(() => {
docIsChanged = true;
}), this.onPositionChange.subscribe(position => {
if (position) {
if (docIsChanged) {
if (scheduler.lastChangesHasLocalUpdate) {
limitPosition(position);
}
else if (this.oldPosition) {
const offset = Math.floor(position.top - this.oldPosition.top);
scroller.setOffset(offset);
}
}
else if (!isPressed) {
if (this.isFixed && this.oldPosition) {
const offset = Math.floor(position.top - this.oldPosition.top);
scroller.setOffset(offset);
}
else {
limitPosition(position);
}
}
}
docIsChanged = false;
}));
}
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 || !nativeRange.collapsed) {
this.positionChangeEvent.next(null);
return;
}
const rect = getLayoutRectByRange(nativeRange);
const { fontSize, lineHeight, color } = 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.floor(Math.max(height, rect.height));
// 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 - containerRect.left);
Object.assign(this.elementRef.style, {
left: left + 'px',
top: top + 'px',
height: boxHeight + 'px',
lineHeight: boxHeight + 'px',
fontSize
});
this.caret.style.backgroundColor = color;
this.styleChangeEvent.next({
height: boxHeight + 'px',
lineHeight: boxHeight + 'px',
fontSize
});
this.positionChangeEvent.next({
left,
top: rectTop,
height: boxHeight
});
}
};
Caret = __decorate([
Injectable(),
__metadata("design:paramtypes", [Scheduler,
Injector])
], Caret);
var DomRenderer_1;
/**
* Textbus PC 端浏览器渲染能力实现
*/
let DomRenderer = DomRenderer_1 = class DomRenderer {
constructor() {
this.isSVG = new RegExp(`^(${[
// 'a',
'animate',
'animateMotion',
'animateTransform',
'circle',
'clipPath',
'defs',
'desc',
'ellipse',
'feBlend',
'feColorMatrix',
'feComponentTransfer',
'feComposite',
'feConvolveMatrix',
'feDiffuseLighting',
'feDisplacementMap',
'feDistantLight',
'feDropShadow',
'feFlood',
'feFuncA',
'feFuncB',
'feFuncG',
'feFuncR',
'feGaussianBlur',
'feImage',
'feMerge',
'feMergeNode',
'feMorphology',
'feOffset',
'fePointLight',
'feSpecularLighting',
'feSpotLight',
'feTile',
'feTurbulence',
'filter',
'foreignObject',
'g',
'image',
'line',
'linearGradient',
'marker',
'mask',
'metadata',
'mpath',
'path',
'pattern',
'polygon',
'polyline',
'radialGradient',
'rect',
// 'script',
'set',
'stop',
// 'style',
'svg',
'switch',
'symbol',
'text',
'textPath',
'title',
'tspan',
'use',
'view'
].join('|')})$`, 'i');
this.xlinkNameSpace = 'http://www.w3.org/1999/xlink';
this.possibleXlinkNames = {
xlinkActuate: 'xlink:actuate',
xlinkactuate: 'xlink:actuate',
'xlink:actuate': 'xlink:actuate',
xlinkArcrole: 'xlink:arcrole',
xlinkarcrole: 'xlink:arcrole',
'xlink:arcrole': 'xlink:arcrole',
xlinkHref: 'xlink:href',
xlinkhref: 'xlink:href',
'xlink:href': 'xlink:href',
xlinkRole: 'xlink:role',
xlinkrole: 'xlink:role',
'xlink:role': 'xlink:role',
xlinkShow: 'xlink:show',
xlinkshow: 'xlink:show',
'xlink:show': 'xlink:show',
xlinkTitle: 'xlink:title',
xlinktitle: 'xlink:title',
'xlink:title': 'xlink:title',
xlinkType: 'xlink:type',
xlinktype: 'xlink:type',
'xlink:type': 'xlink:type'
};
this.formElement = {
input: ['disabled', 'readonly', 'value'],
select: ['disabled', 'readonly'],
option: ['disabled', 'selected', 'value'],
button: ['disabled'],
video: ['controls', 'autoplay', 'loop', 'muted'],
audio: ['controls', 'autoplay', 'loop', 'muted'],
};
}
listen(node, type, callback) {
node.addEventListener(type, callback);
}
unListen(node, type, callback) {
node.removeEventListener(type, callback);
}
createTextNode(textContent) {
return document.createTextNode(DomRenderer_1.replaceEmpty(textContent, '\u00a0'));
}
createElement(name) {
if (this.isSVG.test(name)) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
return document.createElement(name);
}
appendChild(parent, newChild) {
parent.appendChild(newChild);
}
remove(node) {
node.parentNode.removeChild(node);
}
insertBefore(newNode, ref) {
ref.parentNode.insertBefore(newNode, ref);
}
getChildByIndex(parent, index) {
return parent.childNodes[index] || null;
}
addClass(target, name) {
target.classList.add(name);
}
removeClass(target, name) {
target.classList.remove(name);
}
setStyle(target, key, value) {
target.style[key] = value !== null && value !== void 0 ? value : '';
}
removeStyle(target, key) {
target.style[key] = '';
}
setAttribute(target, key, value) {
if (this.possibleXlinkNames[key]) {
this.setXlinkAttribute(target, this.possibleXlinkNames[key], value);
return;
}
target.setAttribute(key, value);
const propNames = this.formElement[target.tagName.toLowerCase()];
if (propNames && propNames.includes(key)) {
target[key] = Boolean(value);
}
}
removeAttribute(target, key) {
if (this.possibleXlinkNames[key]) {
this.removeXlinkAttribute(target, this.possibleXlinkNames[key]);
}
target.removeAttribute(key);
const propNames = this.formElement[target.tagName.toLowerCase()];
if (propNames && propNames.includes(key)) {
target[key] = false;
}
}
setXlinkAttribute(target, key, value) {
target.setAttributeNS(this.xlinkNameSpace, key, value);
}
removeXlinkAttribute(target, key) {
target.removeAttributeNS(this.xlinkNameSpace, key);
}
replace(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild);
}
copy() {
document.execCommand('copy');
}
static replaceEmpty(s, target) {
return s.replace(/\s\s+/g, str => {
return ' ' + Array.from({
length: str.length - 1
}).fill(target).join('');
}).replace(/^\s|\s$/g, target);
}
};
DomRenderer = DomRenderer_1 = __decorate([
Injectable()
], DomRenderer);
var Parser_1;
let Parser = Parser_1 = class Parser {
static parseHTML(html) {
return new DOMParser().parseFromString(html, 'text/html').body;
}
constructor(options, injector) {
var _a;
this.options = options;
this.injector = injector;
const componentLoaders = [
...(options.componentLoaders || [])
];
const formatLoaders = [
...(options.formatLoaders || [])
];
(_a = options.imports) === null || _a === void 0 ? void 0 : _a.forEach(i => {
componentLoaders.push(...(i.componentLoaders || []));
formatLoaders.push(...(i.formatLoaders || []));
});
this.componentLoaders = componentLoaders;
this.formatLoaders = formatLoaders;
}
parseDoc(html, rootComponentLoader) {
const element = Parser_1.parseHTML(html);
return rootComponentLoader.read(element, this.injector, (childSlot, childElement) => {
return this.readSlot(childSlot, childElement);
});
}
parse(html, rootSlot) {
const element = Parser_1.parseHTML(html);
return this.readFormats(element, rootSlot);
}
readComponent(el, slot) {
if (el.nodeType === Node.ELEMENT_NODE) {
if (el.tagName === 'BR') {
slot.insert('\n');
return;
}
for (const t of this.componentLoaders) {
if (t.match(el)) {
const result = t.read(el, this.injector, (childSlot, childElement) => {
return this.readSlot(childSlot, childElement);
});
if (result instanceof 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) {
const textContent = el.textContent;
if (/^\s*[\r\n]+\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;
Array.from(el.childNodes).forEach(child => {
this.readComponent(child, slot);
});
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, childElement) {
this.readFormats(childElement, childSlot);
return childSlot;
}
applyFormats(slot, formatItems) {
slot.background(() => {
formatItems.forEach(i => {
slot.retain(i.startIndex);
slot.retain(i.endIndex - i.startIndex, i.formatter, i.value);
});
});
}
};
Parser = Parser_1 = __decorate([
Injectable(),
__param(0, Inject(EDITOR_OPTIONS)),
__metadata("design:paramtypes", [Object, Injector])
], Parser);
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>
`;
/**
* Textbus PC 端输入实现
*/
let Input = class Input {
constructor(parser, keyboard, commander, selection, controller, caret) {
this.parser = parser;
this.keyboard = keyboard;
this.commander = commander;
this.selection = selection;
this.controller = controller;
this.caret = caret;
this.container = this.createEditableFrame();
this.subscription = new Subscription();
this.textarea = null;
this.isFocus = false;
//
// private inputFormatterId = '__TextbusInputFormatter__'
//
// private inputFormatter: AttributeFormatter = {
// name: this.inputFormatterId,
// type: FormatType.Attribute,
// render: () => {
// return jsx('span', {
// 'data-writing-format': this.inputFormatterId,
// style: {
// textDecoration: 'underline'
// }
// })
// }
// }
this.nativeFocus = false;
this.isSafari = isSafari();
this.isMac = isMac();
this.isWindows = isWindows();
this.isSougouPinYin = false; // 有 bug 版本搜狗拼音
this.onReady = new Promise(resolve => {
this.subscription.add(fromEvent(this.container, 'load').subscribe(() => {
const doc = this.container.contentDocument;
doc.open();
doc.write(iframeHTML);
doc.close();
this.doc = doc;
this.init();
resolve();
}), controller.onReadonlyStateChange.subscribe(() => {
if (controller.readonly) {
this.blur();
}
}));
});
caret.elementRef.append(this.container);
}
focus() {
var _a;
if (this.controller.readonly) {
return;
}
if (!this.isFocus) {
(_a = this.textarea) === null || _a === void 0 ? void 0 : _a.focus();
setTimeout(() => {
var _a, _b, _c;
if (!this.nativeFocus && this.isFocus) {
this.subscription.unsubscribe();
(_b = (_a = this.textarea) === null || _a === void 0 ? void 0 : _a.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(this.textarea);
this.subscription = new Subscription();
this.init();
(_c = this.textarea) === null || _c === void 0 ? void 0 : _c.focus();
}
});
}
this.isFocus = true;
}
blur() {
var _a;
(_a = this.textarea) === null || _a === void 0 ? void 0 : _a.blur();
this.isFocus = false;
}
destroy() {
this.subscription.unsubscribe();
}
init() {
const doc = this.doc;
const contentBody = doc.body;
const textarea = doc.createElement('textarea');
contentBody.appendChild(textarea);
this.textarea = textarea;
this.subscription.add(fromEvent(textarea, 'blur').subscribe(() => {
this.isFocus = false;
this.nativeFocus = false;
this.caret.hide();
}), fromEvent(textarea, 'focus').subscribe(() => {
this.nativeFocus = true;
}), this.caret.onStyleChange.subscribe(style => {
Object.assign(textarea.style, style);
}));
this.handleInput(textarea);
this.handleShortcut(textarea);
this.handleDefaultActions(textarea);
}
handleDefaultActions(textarea) {
this.subscription.add(fromEvent(document, 'copy').subscribe(ev => {
const selection = this.selection;
if (!selection.isSelected) {
return;
}
if (selection.startSlot === selection.endSlot && selection.endOffset - selection.startOffset === 1) {
const content = selection.startSlot.getContentAtIndex(selection.startOffset);
if (typeof content === 'object') {
const clipboardData = ev.clipboardData;
const nativeSelection = document.getSelection();
const range = nativeSelection.getRangeAt(0);
const div = document.createElement('div');
const fragment = range.cloneContents();
div.append(fragment);
clipboardData.setData('text/html', div.innerHTML);
clipboardData.setData('text', div.innerText);
ev.preventDefault();
}
}
}), fromEvent(textarea, 'paste').subscribe(ev => {
const text = ev.clipboardData.getData('Text');
const files = Array.from(ev.clipboardData.files);
if (files.length) {
Promise.all(files.filter(i => {
return /image/i.test(i.type);
}).map(item => {
const reader = new FileReader();
return new Promise(resolve => {
reader.onload = (event) => {
resolve(event.target.result);
};
reader.readAsDataURL(item);
});
})).then(urls => {
const html = urls.map(i => {
return `<img src=${i}>`;
}).join('');
this.handlePaste(html, text);
});
ev.preventDefault();
return;
}
const div = this.doc.createElement('div');
div.style.cssText = 'width:1px; height:10px; overflow: hidden; position: fixed; left: 50%; top: 50%; opacity:0';
div.contentEditable = 'true';
this.doc.body.appendChild(div);
div.focus();
setTimeout(() => {
const html = div.innerHTML;
this.handlePaste(html, text);
this.doc.body.removeChild(div);
});
}));
}
handlePaste(html, text) {
const slot = this.parser.parse(html, new Slot([
ContentType.BlockComponent,
ContentType.InlineComponent,
ContentType.Text
]));
this.commander.paste(slot, text);
}
handleShortcut(textarea) {
let isWriting = false;
let isIgnore = false;
this.subscription.add(fromEvent(textarea, 'compositionstart').subscribe(() => {
isWriting = true;
}), fromEvent(textarea, 'compositionend').subscribe(() => {
isWriting = false;
}), fromEvent(textarea, 'beforeinput').subscribe(ev => {
if (this.isSafari) {
if (ev.inputType === 'insertFromComposition') {
isIgnore = true;
}
}
}), fromEvent(textarea, 'keydown').pipe(filter(() => {
if (this.isSafari && isIgnore) {
isIgnore = false;
return false;
}
return !isWriting; // || !this.textarea.value
})).subscribe(ev => {
let key = ev.key;
const b = key === 'Process' && ev.code === 'Digit2';
if (b) {
key = '@';
this.isSougouPinYin = true;
ev.preventDefault();
}
const is = this.keyboard.execShortcut({
key: key,
altKey: ev.altKey,
shiftKey: ev.shiftKey,
ctrlKey: this.isMac ? ev.metaKey : ev.ctrlKey
});
if (is) {
ev.preventDefault();
}
}));
}
handleInput(textarea) {
this.subscription.add(merge(fromEvent(textarea, 'beforeinput').pipe(filter(ev => {
ev.preventDefault();
if (this.isSafari) {
return ev.inputType === 'insertText' || ev.inputType === 'insertFromComposition';
}
return !ev.isComposing && !!ev.data;
}), map(ev => {
return ev.data;
})), this.isSafari ? new Observable() : fromEvent(textarea, 'compositionend').pipe(map(ev => {
ev.preventDefault();
textarea.value = '';
return ev.data;
}), filter(() => {
const b = this.isSougouPinYin;
this.isSougouPinYin = false;
return !b;
}))).subscribe(text => {
if (text) {
this.commander.write(text);
}
}));
}
// private handleInput(textarea: HTMLTextAreaElement) {
// let index: number
// let offset = 0
// let formats: Formats = []
// this.subscription.add(
// fromEvent<InputEvent>(textarea, 'beforeinput').pipe(
// filter(ev => {
// ev.preventDefault()
// if (isSafari) {
// return ev.inputType === 'insertText'
// // return ev.inputType === 'insertText' || ev.inputType === 'insertFromComposition'
// }
// return !ev.isComposing && !!ev.data
// }),
// map(ev => {
// return ev.data as string
// })
// ).subscribe(text => {
// if (text) {
// this.commander.write(text)
// }
// }),
// fromEvent(textarea, 'compositionstart').subscribe(() => {
// const startSlot = this.selection.startSlot!
// formats = startSlot.extractFormatsByIndex(this.selection.startOffset!)
// formats.push([this.inputFormatter, true])
// this.commander.write('')
// index = this.selection.startOffset!
// }),
// fromEvent<CompositionEvent>(textarea, 'compositionupdate').subscribe((ev) => {
// const text = ev.data
// const startSlot = this.selection.startSlot!
// this.selection.setBaseAndExtent(startSlot, index, startSlot, index + offset)
// this.commander.insert(text, formats)
// offset = text.length
// }),
// fromEvent<CompositionEvent>(textarea, 'compositionend').subscribe((ev) => {
// textarea.value = ''
// const startSlot = this.selection.startSlot!
// this.selection.setBaseAndExtent(startSlot, index, startSlot, index + offset)
// this.commander.insert(ev.data, formats.filter(i => i[0] !== this.inputFormatter))
// offset = 0
// })
// )
// }
createEditableFrame() {
return createElement('iframe', {
attrs: {
scrolling: 'no'
},
styles: {
border: 'none',
width: '100%',
display: 'block',
height: '100%',
position: 'relative',
top: this.isWindows ? '6px' : '0'
}
});
}
};
Input = __decorate([
Injectable(),
__metadata("design:paramtypes", [Parser,
Keyboard,
Commander,
Selection,
Controller,
Caret])
], Input);
/**
* Textbus PC 端选区桥接实现
*/
let SelectionBridge = class SelectionBridge {
constructor(injector, caret, controller, rootComponentRef, input, renderer) {
this.injector = injector;
this.caret = caret;
this.controller = controller;
this.rootComponentRef = rootComponentRef;
this.input = input;
this.renderer = renderer;
this.nativeSelection = document.getSelection();
this.selectionMaskElement = createElement('style');
this.selectionChangeEvent = new Subject();
this.subs = [];
this.connector = null;
this.ignoreSelectionChange = false;
this.changeFromUser = false;
this.docContainer = injector.get(VIEW_DOCUMENT);
this.maskContainer = injector.get(VIEW_MASK);
this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(filter(() => {
return !controller.readonly;
}));
document.head.appendChild(this.selectionMaskElement);
this.sub = this.onSelectionChange.subscribe((r) => {
if (r) {
this.caret.show(r, this.changeFromUser);
}
else {
this.caret.hide();
}
if (r) {
input.focus();
}
else {
input.blur();
}
});
this.sub.add(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;
}
while (target) {
if (target.contentEditable === 'true') {
this.ignoreSelectionChange = true;
return;
}
target = target.parentNode;
}
}));
this.sub.add(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;
}
this.nativeSelection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
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.caret.destroy();
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
};
}
}
findSelectedNodeAndOffset(slot, offset) {
const vElement = this.renderer.getVNodeBySlot(slot);
if (offset >= slot.length) {
const container = this.renderer.getNativeNodeByVNode(vElement);
const lastChild = container.lastChild;
if (lastChild.nodeType === Node.TEXT_NODE) {
return {
node: lastChild,
offset: lastChild.textContent.length
};
}
}
const current = slot.getContentAtIndex(offset);
const prev = slot.getContentAtIndex(offset - 1);
if (current === '\n') {
if (typeof prev === 'string' && prev !== '\n') {
return this.findFocusNativeTextNode(vElement, offset, true);
}
return this.deepFindNativeNodeByOffset(slot, vElement, offset);
}
if (typeof current === 'string') {
return this.findFocusNativeTextNode(vElement, offset, typeof prev === 'string' && prev !== '\n');
}
if (prev === '\n') {
if (typeof current === 'undefined') {
const container = this.renderer.getNativeNodeByVNode(vElement);
return {
node: container,
offset: container.childNodes.length
};
}
for (const component of slot.sliceContent().filter((i) => {
return typeof i !== 'string';
})) {
if (component === current) {
const vNode = this.renderer.getVNodeByComponent(component);
const nativeNode = this.renderer.getNativeNodeByVNode(vNode);
return {
node: nativeNode.parentNode,
offset: Array.from(nativeNode.parentNode.childNodes).indexOf(nativeNode)
};
}
}
}
if (typeof prev === 'string') {
return this.findFocusNativeTextNode(vElement, offset, true);
}
if (typeof prev === 'undefined') {
const vNode = this.renderer.getVNodeByComponent(current);
const nativeNode = this.renderer.getNativeNodeByVNode(vNode);
return {
node: nativeNode.parentNode,
offset: Array.from(nativeNode.parentNode.childNodes).indexOf(nativeNode)
};
}
const vNode = this.renderer.getVNodeByComponent(prev);
const nativeNode = this.renderer.getNativeNodeByVNode(vNode);
return {
node: nativeNode.parentNode,
offset: Array.from(nativeNode.parentNode.childNodes).indexOf(nativeNode) + 1
};
// return this.deepFindNativeNodeByOffset(slot, vElement, offset)
}
findFocusNativeTextNode(vElement, offset, toLeft) {
for (const item of vElement.children) {
const position = this.renderer.getLocationByVNode(item);
if (toLeft ? position.endIndex < offset : position.endIndex <= offset) {
continue;
}
if (item instanceof VTextNode) {
return {
node: this.renderer.getNativeNodeByVNode(item),
offset: offset - position.startIndex
};
}
return this.findFocusNativeTextNode(item, offset, toLeft);
}
return null;
}
deepFindNativeNodeByOffset(source, root, offset) {
for (const item of root.children) {
const position = this.renderer.getLocationByVNode(item);
if (position.slot !== source) {
return null;
}
if (position.endIndex <= offset) {
continue;
}
if (item instanceof VElement) {
if (position.startIndex === offset && position.endIndex === offset + 1) {
const position = this.deepFindNativeNodeByOffset(source, item, offset);
if (position) {
return position;
}
const node = this.renderer.getNativeNodeByVNode(item);
const parent = node.parentNode;
return {
node: parent,
offset: Array.from(parent.childNodes).indexOf(node)
};
}
return this.deepFindNativeNodeByOffset(source, item, offset);
}
}
return null;
}
unListen() {
this.subs.forEach(i => i.unsubscribe());
this.subs = [];
}
listen(connector) {
const selection = this.nativeSelection;
this.subs.push(fromEvent(this.docContainer, 'mousedown').subscribe(ev => {
if (this.ignoreSelectionChange || ev.button === 2) {
return;
}
if (!ev.shiftKey) {
selection.removeAllRanges();
}
}), fromEvent(document, 'selectionchange').subscribe(() => {
this.syncSelection(connector);
}));
}
syncSelection(connector) {
var _a;
const selection = this.nativeSelection;
this.changeFromUser = true;
if (this.ignoreSelectionChange ||
selection.rangeCount === 0 ||
!this.docContainer.contains(selection.anchorNode) ||
this.rootComponentRef.component.slots.length === 0) {
return;
}
const nativeRange = selection.getRangeAt(0).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 vEle = this.renderer.getVNodeBySlot(this.rootComponentRef.component.slots.first);
if (!vEle) {
return;
}
const nativeNode = this.renderer.getNativeNodeByVNode(vEle);
if (!nativeNode) {
return;
}
nativeRange.setEndAfter(nativeNode.lastChild);
}
else {
const vEle = this.renderer.getVNodeBySlot(this.rootComponentRef.component.slots.last);
if (!vEle) {
return;
}
const nativeNode = this.renderer.getNativeNodeByVNode(vEle);
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);
this.selectionChangeEvent.next(nativeRange);
}
else {
connector.setSelection(null);
}
return;
}
connector.setSelection(null);
}
getCorrectedPosition(node, offset, toAfter, excludeNodes = []) {
excludeNodes.push(node);
if (node.nodeType === Node.ELEMENT_NODE) {
const containerPosition = this.renderer.getLocationByNativeNode(node);
const childNode = node.childNodes[offset];
if (childNode) {
const childPosition = this.renderer.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.renderer.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.renderer.getLocationByNativeNode(node);
if (containerPosition) {
return {
slot: containerPosition.slot,
offset: containerPosition.startIndex + offset
};
}
const nextNode = toAfter ? node.nextSibling : node.previousSibling;
if (nextNode) {