/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import { html, TemplateResult } from 'lit'; import { customElement, query, state, property } from 'lit/decorators.js'; import { styles } from './nile-content-editor.css'; import { CSSResultGroup } from 'lit'; import { classMap } from 'lit/directives/class-map.js'; import NileElement from '../internal/nile-element'; import { watch } from '../internal/watch'; import { KeyCode } from '../internal/enum'; import { live } from 'lit/directives/live.js'; /** * Nile icon component. * * @tag nile-attribute-expression * */ @customElement('nile-content-editor') export class NileContentEditor extends NileElement { /** * @summary Allows you to handle both input and dropdown selection * @dependency nile-option * @event nile-change - Emitted when the control's value changes. */ @property() value: string = ''; @property() options: Array; @property() filteredOptions: Array; @property() showLabel: boolean = true; @property() labelText = ''; @property() type = 'text|text-area'; @property() required: boolean = true; @query('.content-editable-input') contentEditor: HTMLInputElement; @query('.nile-options-container') autoOptions: HTMLInputElement; @state() openDropdown = false; tagIdentifier = '$'; filteredValue = ''; @property({ attribute: 'help-text' }) helpText = ''; @property({ attribute: 'readonly' }) readonly = false; @property({ attribute: 'error-message' }) errorMessage = ''; @property({ attribute: 'error' }) error = false; @property({ attribute: 'noborder' }) noborder = false; @property({ type: Boolean }) updateValue: any = false; @state() initialValue = ''; static styles: CSSResultGroup = styles; connectedCallback() { this.handleOutsideClick = this.handleOutsideClick.bind(this); this.handleClipboardEvent = this.handleClipboardEvent.bind(this); super.connectedCallback(); this.emit('nile-init'); this.addOpenListeners(); this.setInitialValues(); } addOpenListeners() { window.addEventListener('click', this.handleOutsideClick); this.addEventListener('cut', this.handleClipboardEvent); this.addEventListener('copy', this.handleClipboardEvent); this.addEventListener('paste', this.handleClipboardEvent); } removeOpenListeners() { this.removeEventListener('click', this.handleOutsideClick); this.removeEventListener('cut', this.handleClipboardEvent); this.removeEventListener('copy', this.handleClipboardEvent); this.removeEventListener('paste', this.handleClipboardEvent); } handleOutsideClick(event: any) { if (event && event.target && !this.contains(event.target)) { this.toggleDropdown(false); } } async handleClipboardEvent(event: Event) { const selectedText = window?.getSelection()?.toString(); switch (event.type) { case 'cut': case 'copy': if (!!selectedText) await navigator.clipboard.writeText(selectedText); document.execCommand(event.type); break; case 'paste': event.preventDefault(); let pastedText = ''; pastedText = await navigator.clipboard.readText(); pastedText = pastedText .replace(/<(|\/)(html|body|meta|span)[^>]*?>/gi, '') .trim(); document.execCommand('insertText', false, pastedText); break; default: break; } setTimeout(() => { this.emitInputChange(); }); } toggleDropdown(value: boolean) { this.openDropdown = value; } setInitialValues() { this.updateComplete.then(res => { if (res) this.contentEditor.innerHTML = this.generateHTMLTagsWithValues( this.value ); }); } @watch(['type', 'value'], { waitUntilFirstUpdate: true }) handleTypeChange() { if (this.updateValue) { this.contentEditor.innerHTML = this.generateHTMLTagsWithValues( this.value ); } } disconnectedCallback(): void { super.disconnectedCallback(); this.removeOpenListeners(); this.emit('nile-destroy'); } private handlekeyDown(event: any) { if (event.code === KeyCode.ENTER && this.type === 'text') { event.preventDefault(); return; } if ( [KeyCode.CUT, KeyCode.COPY, KeyCode.PASTE].includes(event.code) && (event.ctrlKey || event.metaKey) ) { return; } if (event.code === KeyCode.ESCAPE) { event.preventDefault(); this.toggleDropdown(false); return; } setTimeout(() => { const value = this.contentEditor.innerText; this.emitInputChange(); if (value.includes(this.tagIdentifier)) { this.insertNodes(this.contentEditor, this.contentEditor.childNodes); this.filterOptions(); this.toggleDropdown(true); } else { this.toggleDropdown(false); } }, 10); } filterOptions() { this.filteredOptions = this.options; this.filteredValue = this.filteredValue.trim(); if (!!this.options.length && !!this.filteredValue) { this.filteredOptions = this.options.filter(item => item.includes(this.filteredValue) ); } else { this.filteredOptions = this.options; } } generateValuesFromHTMLTags(string: any): string { let errorTag = /(.*?)<\/span>/g; this.error = !!string.match(errorTag); var pattern = /(.*?)<\/span>/g; let result = string.replace(pattern, (match: any, option: any) => { return '{{' + option + '}}'; }); return result; } generateHTMLTagsWithValues(string: any): string { var pattern = /{{(.*?)}}/g; var result = string.replace(pattern, (match: any, option: any) => { return this.options.includes(option) ? '' + option + '' : this.setError(option); }); return result; } setError(option: any) { this.error = true; return ( '' + option + '' ); } emitInputChange(): void { if (this.contentEditor) { let fieldValue = this.contentEditor.innerHTML; fieldValue = fieldValue.replace(/ /g, ' '); fieldValue = fieldValue.replace('
', ''); this.emit('nile-change', { value: this.generateValuesFromHTMLTags(fieldValue), }); } } replaceText = ''; insertNodes( parentNode: Node, childNodes: NodeList, autoOptionsTag?: HTMLElement ) { [...childNodes].forEach((node: Node, index: number) => { if (node.hasChildNodes()) { if (autoOptionsTag) { this.insertNodes(node, node.childNodes, autoOptionsTag); } else { this.insertNodes(node, node.childNodes); } } else { if (node.nodeValue?.includes(this.tagIdentifier)) { if (autoOptionsTag) { this.insertAutoOptionsTag(node, autoOptionsTag); } this.setFilterValue(node.nodeValue); return; } } }); } insertAutoOptionsTag(node: any, autoOptionsTag: any) { const selection = window.getSelection(); const range = document.createRange(); const curssorNodeindex = node.nodeValue.indexOf(this.tagIdentifier); range.setStart(node, curssorNodeindex); range.insertNode(autoOptionsTag); range.setStartAfter(autoOptionsTag); range.collapse(true); selection?.removeAllRanges(); selection?.addRange(range); if (autoOptionsTag.nextSibling?.nodeValue) { autoOptionsTag.nextSibling.nodeValue = autoOptionsTag.nextSibling?.nodeValue?.replace( this.tagIdentifier + this.filteredValue, '' ); } this.contentEditor.focus(); } setFilterValue(value: any) { //replace Text - check if text exists after tagidentifier , if exists take account of that too if (!this.openDropdown) { this.replaceText = value.split(this.tagIdentifier).slice(1).join(); } if (!!value && this.openDropdown) { this.filteredValue = value ?.split(this.tagIdentifier) .slice(1) .join() .replace(this.replaceText, ''); } else { this.filteredValue = ''; } } handleOptions(option: any): void { this.toggleDropdown(false); let autoOptionsTag = document.createElement('span'); autoOptionsTag.setAttribute('class', 'chips'); autoOptionsTag.setAttribute('contentEditable', 'false'); autoOptionsTag.innerText = option; this.insertNodes( this.contentEditor, this.contentEditor.childNodes, autoOptionsTag ); this.resetOptions(); this.emitInputChange(); } resetOptions() { this.filteredOptions = this.options; this.filteredValue = ''; } public renderAutoOptions(): TemplateResult { return html`
${this.filteredOptions && this.filteredOptions.map((option: any) => { return html` ${option} `; })}
`; } public render(): TemplateResult { const hasHelpText = this.helpText ? true : false; const hasError = !!this.error; const hasErrorMessage = !!this.errorMessage; const readonly = !!this.readonly; const noborder = !!this.noborder; const type = this.type; return html`
${this.showLabel && this.labelText ? html` ${this .required ? html`*` : ''}` : ''}
${hasHelpText ? html` ${this.helpText} ` : ``} ${hasErrorMessage ? html` ${this.errorMessage} ` : ``}
${this.filteredOptions && !!this.filteredOptions.length ? this.renderAutoOptions() : null}
`; } } export default NileContentEditor; declare global { interface HTMLElementTagNameMap { 'nile-content-editor': NileContentEditor; } }