libs/elements/src/lib/components/text-editor/text-editor.tsx
import { renderHiddenInput } from '@aiao/elements-cdk';import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State, Watch } from '@stencil/core'; import { InputChangeEventDetail } from '../../interfaces/input.interface';import { docGetSelection, getSelectionElements, restoreRange, saveRange } from '../../utils/selection';import { TextActionState } from '../text-editor-bar/text-editor-bar.interface';import { TextEditorAcitons as TA } from './text-editor.interface'; const IGNORE_ACTIONS = [TA.heading, TA.undo, TA.redo, TA.createLink, TA.indent, TA.outdent, TA.quote];let richTextEditorInputId = 0;// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand @Component({ tag: 'aiao-text-editor', styleUrl: 'text-editor.scss', shadow: true})export class RichTextEditor { @Element() el!: HTMLElement; private range?: Range; private _lastElement?: HTMLElement; private inputId = `aiao-text-editor:${richTextEditorInputId++}`; // --------------------------------------------------------------[ Event ] /** * 值改变 */ @Event() aiaoChange!: EventEmitter<InputChangeEventDetail>; @Event() aiaoStateChange!: EventEmitter<TextActionState>; // --------------------------------------------------------------[ State ] @State() editable = false; @State() _state: TextActionState = {}; // --------------------------------------------------------------[ Prop ] /** * 显示命令条 */ @Prop() showActionBar = true; /** * 段落符 */ @Prop() defaultParagraphSeparator = 'p'; /** * 禁用 */ @Prop() disabled = false; /** * 绑定的 dom 元素 */ @Prop() element?: HTMLElement; /** * form name */ @Prop() name = this.inputId; /** * form value */ @Prop({ mutable: true }) value = ''; // --------------------------------------------------------------[ Watch ] @Watch('element') elementChanged(value?: HTMLElement) { if (this._lastElement !== value && value) { if (this._lastElement) { this._lastElement.removeEventListener('keyup', this.statChangeHander); this._lastElement.removeEventListener('mouseup', this.statChangeHander); this._lastElement.removeEventListener('input', this.onInput); this._lastElement.removeEventListener('focus', this.onFocus); this._lastElement.removeEventListener('blur', this.onBlur); } this._lastElement = value; this._lastElement.addEventListener('keyup', this.statChangeHander); this._lastElement.addEventListener('mouseup', this.statChangeHander); this._lastElement.addEventListener('input', this.onInput); this._lastElement.addEventListener('focus', this.onFocus); this._lastElement.addEventListener('blur', this.onBlur); this._lastElement.contentEditable = 'true'; } } // // --------------------------------------------------------------[ public function ] /** * 得到选中的标签 */ @Method() async getSelectionElements() { return getSelectionElements(document); } /** * 记录选择位置 */ @Method() async saveSelection() { this._saveSelection(); } /** * 恢复选择位置 */ @Method() async restoreSelection() { this._restoreSelection(); } async action( action: | TA.bold | TA.indent | TA.insertHorizontalRule | TA.insertOrderedList | TA.insertUnorderedList | TA.italic | TA.redo | TA.strikeThrough | TA.underline | TA.undo | TA.unlink | TA.outdent | TA.justifyCenter | TA.justifyFull | TA.justifyLeft | TA.justifyRight ): Promise<any>; async action( action: TA.foreColor | TA.createLink | TA.foreColor | TA.insertHTML | TA.insertImage, value: string ): Promise<any>; async action(action: TA.fontSize | TA.heading, value: number): Promise<any>; /** * 执行命令 * @param action 命令 * @param value 值 */Function `action` has 47 lines of code (exceeds 25 allowed). Consider refactoring.
Function `action` has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring. @Method() async action(action: TA, value?: any) { switch (action) { case TA.bold: case TA.indent: case TA.insertHorizontalRule: case TA.insertOrderedList: case TA.insertUnorderedList: case TA.italic: case TA.justifyCenter: case TA.justifyFull: case TA.justifyLeft: case TA.justifyRight: case TA.outdent: case TA.redo: case TA.strikeThrough: case TA.underline: case TA.undo: case TA.unlink: this.exec(action); break; case TA.backColor: case TA.createLink: case TA.foreColor: case TA.insertHTML: case TA.insertImage: this.exec(action, (value || '').trim()); break; case TA.fontSize: this.exec(action, value); break; case TA.heading: this.exec('formatBlock', `h${value}`); break; case TA.paragraph: case TA.quote: const { insertOrderedList, insertUnorderedList } = this._state; if (!(insertOrderedList || insertUnorderedList)) { if (action === TA.paragraph) { this.exec('formatBlock', value || this.paragraphSeparator); } else { this.exec('formatBlock', 'blockquote'); } } break; default: break; } this.statChangeHander(); } // --------------------------------------------------------------[ Listen ] onInput = () => { this.checkEle(); this.value = this._lastElement!.innerHTML; this.aiaoChange.emit({ value: this.value }); }; onFocus = (_: Event) => { // }; onBlur = (_: Event) => { // }; // --------------------------------------------------------------[ public function ] private _restoreSelection() { if (this.range) { restoreRange(document, this.range); } } private _saveSelection() { this.range = saveRange(document); } // --------------------------------------------------------------[ event Handler ] private onAction = (event: CustomEvent<any>) => { const { action, value } = event.detail; this.action(action, value); }; // --------------------------------------------------------------[ private function ] private statChangeHander = () => { this._state = this.getState(); this.aiaoStateChange.emit(this._state); }; private get paragraphSeparator() { return this.defaultParagraphSeparator || 'p'; } // 检测元素, 保证文本使用 p 标签 private checkEle() { if (this._lastElement?.firstChild && this._lastElement.firstChild.nodeType === 3) { this.exec('formatBlock', this.paragraphSeparator); } } private getSelectionAttributeValue(attribute: string) { let selection = docGetSelection(document); if (this._lastElement) { selection = docGetSelection(this._lastElement.ownerDocument); } if (selection.rangeCount < 1) { return ''; } const range = selection.getRangeAt(0); const pe = range.commonAncestorContainer.parentElement; return pe ? pe.getAttribute(attribute) : ''; } Function `getState` has 29 lines of code (exceeds 25 allowed). Consider refactoring. private getState() { const backState: TextActionState = {}; const fb = this.queryCommandValue('formatBlock'); if (fb === 'blockquote') { backState[TA.quote] = true; } // heading const heading = /^h(?<heading>\d+)$/i.exec(fb)?.groups?.heading; if (heading) { backState[TA.heading] = +heading; } // link const href = this.getSelectionAttributeValue('href'); if (href) { backState.createLink = href; } Object.keys(TA) .filter(key => IGNORE_ACTIONS.includes(key as any) === false) .forEach(key => { let val: any = this.queryCommandValue(key); if (val === 'false' || val === '') { return; } if (val === 'true') { val = true; } backState[key] = val; }); if (backState.fontSize) { backState.fontSize = +backState.fontSize; } return backState; } private queryCommandValue(commandId: string) { return document.queryCommandValue(commandId); } private exec(commandId: any, value?: any) { return document.execCommand(commandId, true, value); } // --------------------------------------------------------------[ lifecycle ] componentDidLoad() { const ele = this.element || this._lastElement; if (ele) { this.elementChanged(ele); ele.innerHTML = this.value || ''; } } render() { renderHiddenInput(true, this.el, this.name, this.value, this.disabled); const cls = { 'inline-element': !this.element }; return ( <Host class={cls}> {this.showActionBar && ( <aiao-text-editor-bar actionState={this._state} onAction={this.onAction}></aiao-text-editor-bar> )} {!this.element && <div class="element" ref={e => this.elementChanged(e)}></div>} </Host> ); }}