import { AtomBinder } from "@web-atoms/core/dist/core/AtomBinder"; import Bind from "@web-atoms/core/dist/core/Bind"; import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty"; import XNode from "@web-atoms/core/dist/core/XNode"; import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl"; import AtomRepeater from "../basic/AtomRepeater"; import AddImage, { showImageDialog } from "./commands/AddImage"; import AddLink from "./commands/AddLink"; import Align from "./commands/Align"; import AttachFile from "./commands/AttachFile"; import Bold from "./commands/Bold"; import ChangeColor from "./commands/ChangeColor"; import ChangeFont from "./commands/ChangeFont"; import ChangeFontSize from "./commands/ChangeFontSize"; import Headings from "./commands/Headings"; import HorizontalRule from "./commands/HorizontalRule"; import IndentLess from "./commands/IndentLess"; import IndentMore from "./commands/IndentMore"; import Italic from "./commands/Italic"; import NumberedList from "./commands/NumberedList"; import RemoveFormat from "./commands/RemoveFormat"; import Separator from "./commands/Separator"; import Source from "./commands/Source"; import StrikeThrough from "./commands/StrikeThrough"; import Underline from "./commands/Underline"; import Unlink from "./commands/Unlink"; import UnorderedList from "./commands/UnorderedList"; import Toolbar from "./commands/Toolbar"; export { default as Toolbar} from "./commands/Toolbar"; import "../styles/atom-html-editor.global.css"; const link = document.createElement("link"); link.href = "https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"; link.rel = "preload"; link.as = "style"; link.onload = () => { link.onload = null; link.rel = "stylesheet"; }; document.head.appendChild(link); function preventLinkClick(e: Event, editor: HTMLElement, doc: Document) { let target = e.target as HTMLElement; const body = doc.body; while (target) { if (target.getAttribute("data-prompt")) { break; } if (target === body) { break; } if (target.isContentEditable) { editor.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); return; } if (target.tagName === "A") { e.preventDefault(); } target = target.parentElement; } target = e.target as HTMLElement; const originalTarget = target; const data = new Proxy(originalTarget, { get(target, p) { if (typeof p !== "string") { return; } while (target) { const value = target.dataset[p]; if (value !== void 0) { return value; } target = target.parentElement; } } }); editor.dispatchEvent(new CustomEvent("editorClick", { detail: { target, data }, bubbles: true, cancelable: true } )); editor.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); // while (target && target !== body) { // const ds = target.dataset; // const command = ds.command; // if (command) { // let commandParameter = ds.commandParameter; // if (!commandParameter) { // commandParameter = ds.commandParameters; // if (commandParameter) { // commandParameter = JSON.parse(commandParameter); // } // } // // this will force currentTarget/lastTarget to be updated // // in navigation and popup service // editor.dispatchEvent(new CustomEvent("click", { // bubbles: true // })); // editor.dispatchEvent(new CustomEvent("command", { // bubbles: true, // detail: { // target, // command, // commandParameter // } // })); // e.preventDefault(); // return; // } // const click = ds.click; // if (click) { // editor.dispatchEvent(new CustomEvent("click", { // bubbles: true // })); // editor.dispatchEvent(new CustomEvent("htmlEditorClick", { // detail: { // target, // command: click // } // })); // } // if (target.isContentEditable) { // break; // } // if (target.tagName === "A") { // editor.dispatchEvent(new CustomEvent("click", { // bubbles: true // })); // e.preventDefault(); // return false; // } // target = target.parentElement; // } } export interface IEditorCommand { target: HTMLElement; command: string; commandParameter: string; } export interface ITagCommand { name: string; style: any; handler: (ce: CustomEvent<{ target: HTMLElement, data: T}>) => any } export default class AtomHtmlEditor extends AtomControl { @BindableProperty public content: string; @BindableProperty public header: any[]; @BindableProperty public version: number; @BindableProperty public files: File[]; public editor: HTMLDivElement; @BindableProperty public tags: ITagCommand[]; public eventDocumentCreated: (e: CustomEvent) => void; public eventDocumentUpdated: (e: CustomEvent) => void; public eventCommand: (e: CustomEvent) => void; public get htmlContent(): string { try { return this.editor?.innerHTML ?? this.initialContent; } catch (ex) { // tslint:disable-next-line: no-console console.warn(ex); } } public set htmlContent(v: string) { if (this.editor) { this.editor.innerHTML = v; } else { this.initialContent = v; } AtomBinder.refreshValue(this, "htmlContent"); } private editorWindow: Window; private editorDocument: Document; private initialContent: string; public insertImage(s: AtomHtmlEditor, e: Event) { return showImageDialog(s, e); } public executeCommand(cmd, showUI?: boolean, value?: string): boolean { const r = this.editorDocument.execCommand(cmd, showUI, value); setTimeout(() => this.version++, 1); return r; } public queryCommandState(cmd: string, ... a: any[]): boolean { return this.editorDocument.queryCommandState(cmd); } public getStyle(name: string, v: any): string { try { const node = this.editorWindow.getSelection().getRangeAt(0); const e = node.startContainer.parentElement as HTMLElement; return this.editorWindow.getComputedStyle(e)[name]; } catch (ex) { return null; } } public onPropertyChanged(name) { super.onPropertyChanged(name); if (name === "tags") { this.setupTags(); } } protected setupTags() { const doc = this.editorDocument; if (!doc) { return; } if (Array.isArray(this.tags)) { for (const { name, style } of this.tags) { if (style) { const styleElement = doc.createElement("style"); styleElement.textContent = `*[data-command=${name}] { ${style.toStyleSheet()} }` doc.head.appendChild(styleElement); } } } } protected preCreate() { this.version = 1; this.files = []; this.runAfterInit(() => { this.setup(); }); this.element.classList.add("atom-control-html-editor"); this.element.classList.add("html-editor"); } protected onPasteEvent(e: ClipboardEvent) { // tslint:disable-next-line: no-console if (!e.clipboardData.types.find((x) => x === "text/html")) { return; } const d = e.clipboardData.getData("text/plain"); if (d) { // we need to sanitize this one... const s = window.getSelection(); const r = s.getRangeAt(0); let last: HTMLElement; const tags = []; for (const iterator of d.split("\n")) { const text = iterator.trim(); if (!text) { continue; } last = document.createElement("p"); last.innerText = iterator; tags.push(last); } while(tags.length) { r.insertNode(tags.pop()); } r.setStartAfter(last); } e.preventDefault(); } protected onInputEvent(e: InputEvent) { // tslint:disable-next-line: no-console console.log(e); } protected setup() { const frame = this.element.getElementsByTagName("iframe")[0] as HTMLIFrameElement; const doc = frame.contentWindow.document; doc.open(); const baseUrl = `${location.protocol}//${location.host}`; doc.writeln(`

 

`); doc.close(); const style = doc.createElement("style"); style.textContent = `body { font-family: arial,sans-serif } #editor { min-height: 500px; } *[data-prompt] { background: yellow } `; doc.head.appendChild(style); // const script = doc.createElement("script"); // script.textContent = `document.body.addEventListener("click", ${preventLinkClick.toString()});`; // doc.body.appendChild(script); doc.body.addEventListener("click", (e) => preventLinkClick(e, this.element, doc)); this.editor = doc.getElementById("editor") as HTMLDivElement; this.editor.contentEditable = "true"; this.editor.innerHTML = this.initialContent ?? ""; doc.execCommand("styleWithCSS"); doc.execCommand("insertBrOnReturn"); const updateVersion = () => setTimeout(() => { this.version++; AtomBinder.refreshValue(this, "htmlContent"); this.element.dispatchEvent(new CustomEvent("documentUpdated", { detail: this.editor })); this.documentUpdated(this.editor); }, 1); this.editor.addEventListener("click", updateVersion); this.editor.addEventListener("keydown", updateVersion); this.editor.addEventListener("keypress", updateVersion); this.editor.addEventListener("input", updateVersion); this.editorWindow = frame.contentWindow; this.editorDocument = doc; updateVersion(); this.documentCreated(this.editor); this.element.dispatchEvent(new CustomEvent("documentCreated", { detail: this.editor })); this.registerDisposable({ dispose: () => { this.editor.removeEventListener("click", updateVersion); this.editor.removeEventListener("keydown", updateVersion); this.editor.removeEventListener("keypress", updateVersion); this.editor.removeEventListener("input", updateVersion); } }); this.bindEvent(this.element, "editorClick", (ce: CustomEvent) => { const { detail: { data, target} } = ce; if (data.prompt) { const element = target as HTMLElement; const result = prompt("Enter " + (data.promptTitle ?? data.prompt), data.promptDefault); if (result) { const replace = ((data.replace ?? "textContent") as string).split(",").map((x) => x.trim()); for (const iterator of replace) { const template = element.getAttribute("data-" + iterator + "-template") || element.getAttribute("data-template"); if (template) { element[iterator] = template.replace(data.prompt, result); } else { element[iterator] = result; } } } return; } if(this.tags) { for (const { name, handler } of this.tags) { if (name === data.command || name === data.clickEvent) { handler(ce); } } } }); this.setupTags(); } protected documentCreated(e: HTMLDivElement) { // nothing... } protected documentUpdated(e: HTMLDivElement) { // nothing... } protected render(node: XNode, e?: any, creator?: any): void { // following line will prevent stack overflow this.render = super.render; if (!node.children || node.children.length === 0 ) { node.children[0] = ; } super.render(
{ ... node.children as any[]} this.files.remove(ce.detail)} items={Bind.oneWay(() => this.files)} itemRenderer={(file: File) =>
} />