@textbus/browser
Version:
Textbus is a rich text editor and framework that is highly customizable and extensible to achieve rich wysiwyg effects.
1,346 lines (1,330 loc) • 72.7 kB
JavaScript
'use strict';
require('reflect-metadata');
var stream = require('@tanbo/stream');
var di = require('@tanbo/di');
var core = require('@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 di.InjectionToken('EDITOR_OPTIONS');
/**
* 编辑器容器依赖注入 token
*/
const VIEW_CONTAINER = new di.InjectionToken('VIEW_CONTAINER');
/**
* 编辑器容器依赖注入 token
*/
const VIEW_DOCUMENT = new di.InjectionToken('VIEW_DOCUMENT');
/**
* 编辑器容器遮罩层 token
*/
const VIEW_MASK = new di.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();
}
exports.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 stream.Subject();
this.styleChangeEvent = new stream.Subject();
this.oldRange = null;
this.isFixed = false;
this.editorMask = injector.get(VIEW_MASK);
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.subs.push(stream.fromEvent(document, 'mousedown').subscribe(() => {
this.flashing = false;
}), stream.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;
}
}), stream.fromEvent(document, 'mousedown', true).subscribe(() => {
isPressed = true;
}), stream.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
});
}
};
exports.Caret = __decorate([
di.Injectable(),
__metadata("design:paramtypes", [core.Scheduler,
di.Injector])
], exports.Caret);
var DomRenderer_1;
/**
* Textbus PC 端浏览器渲染能力实现
*/
exports.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);
}
};
exports.DomRenderer = DomRenderer_1 = __decorate([
di.Injectable()
], exports.DomRenderer);
var Parser_1;
exports.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 core.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);
});
});
}
};
exports.Parser = Parser_1 = __decorate([
di.Injectable(),
__param(0, di.Inject(EDITOR_OPTIONS)),
__metadata("design:paramtypes", [Object, di.Injector])
], exports.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 端输入实现
*/
exports.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 stream.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(stream.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 stream.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(stream.fromEvent(textarea, 'blur').subscribe(() => {
this.isFocus = false;
this.nativeFocus = false;
this.caret.hide();
}), stream.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(stream.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();
}
}
}), stream.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 core.Slot([
core.ContentType.BlockComponent,
core.ContentType.InlineComponent,
core.ContentType.Text
]));
this.commander.paste(slot, text);
}
handleShortcut(textarea) {
let isWriting = false;
let isIgnore = false;
this.subscription.add(stream.fromEvent(textarea, 'compositionstart').subscribe(() => {
isWriting = true;
}), stream.fromEvent(textarea, 'compositionend').subscribe(() => {
isWriting = false;
}), stream.fromEvent(textarea, 'beforeinput').subscribe(ev => {
if (this.isSafari) {
if (ev.inputType === 'insertFromComposition') {
isIgnore = true;
}
}
}), stream.fromEvent(textarea, 'keydown').pipe(stream.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(stream.merge(stream.fromEvent(textarea, 'beforeinput').pipe(stream.filter(ev => {
ev.preventDefault();
if (this.isSafari) {
return ev.inputType === 'insertText' || ev.inputType === 'insertFromComposition';
}
return !ev.isComposing && !!ev.data;
}), stream.map(ev => {
return ev.data;
})), this.isSafari ? new stream.Observable() : stream.fromEvent(textarea, 'compositionend').pipe(stream.map(ev => {
ev.preventDefault();
textarea.value = '';
return ev.data;
}), stream.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'
}
});
}
};
exports.Input = __decorate([
di.Injectable(),
__metadata("design:paramtypes", [exports.Parser,
core.Keyboard,
core.Commander,
core.Selection,
core.Controller,
exports.Caret])
], exports.Input);
/**
* Textbus PC 端选区桥接实现
*/
exports.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 stream.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(stream.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(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;
}
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;
}
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 core.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 core.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(stream.fromEvent(this.docContainer, 'mousedown').subscribe(ev => {
if (this.ignoreSelectionChange || ev.button === 2) {
return;
}
if (!ev.shiftKey) {
selection.removeAllRanges();
}
}), stream.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 ne