diff --git a/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts b/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts index 708f63e..dc92f09 100644 --- a/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts +++ b/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts @@ -21,6 +21,7 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor { value: string = ''; disabled: boolean = false; + messageTemplateSchema = this.createCustomSchema(); customCssClass = '.MD2Icon{font-family: "Massive Darkness 2", sans-serif !important; font-size: 40px; margin-left:5px} body{font-size: 30px; }'; // ControlValueAccessor interface @@ -109,4 +110,303 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor { } }); } + + // Text manipulation methods for Kendo Editor + /** + * Parses HTML string to ProseMirror nodes + * @param htmlContent - HTML string to parse + * @returns ProseMirror Fragment + */ + private parseHtmlContent(htmlContent: string): any { + if (!this.editor || !this.editor.view) return null; + + const view = this.editor.view; + const element = document.createElement('div'); + element.innerHTML = htmlContent; + + // Use ProseMirror's DOMParser to parse HTML into nodes + const parser = ProseMirrorDOMParser.fromSchema(view.state.schema); + const slice = parser.parseSlice(element); + return slice.content; + } + + /** + * Inserts HTML content at the beginning of the editor + * @param content - HTML string to insert + * @param asHtml - If true, parses as HTML; if false, inserts as plain text + */ + public insertAtBeginning(content: string, asHtml: boolean = false): void { + if (!this.editor || !this.editor.view) return; + + const view = this.editor.view; + const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content); + + if (contentNode) { + const transaction = view.state.tr.insert(0, contentNode); + view.dispatch(transaction); + this.onValueChanged(this.editor.value); + } + } + + /** + * Inserts HTML content at the end of the editor + * @param content - HTML string to insert + * @param asHtml - If true, parses as HTML; if false, inserts as plain text + */ + public insertAtEnd(content: string, asHtml: boolean = false): void { + if (!this.editor || !this.editor.view) return; + + const view = this.editor.view; + const endPos = view.state.doc.content.size; + const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content); + + if (contentNode) { + const transaction = view.state.tr.insert(endPos, contentNode); + view.dispatch(transaction); + this.onValueChanged(this.editor.value); + } + } + + /** + * Inserts HTML content before the current selection + * @param content - HTML string to insert + * @param asHtml - If true, parses as HTML; if false, inserts as plain text + */ + public insertBeforeSelection(content: string, asHtml: boolean = false): void { + if (!this.editor || !this.editor.view) return; + + const view = this.editor.view; + const { from } = view.state.selection; + const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content); + + if (contentNode) { + const transaction = view.state.tr.insert(from, contentNode); + view.dispatch(transaction); + this.onValueChanged(this.editor.value); + } + } + + /** + * Inserts HTML content after the current selection + * @param content - HTML string to insert + * @param asHtml - If true, parses as HTML; if false, inserts as plain text + */ + public insertAfterSelection(content: string, asHtml: boolean = false): void { + if (!this.editor || !this.editor.view) return; + + const view = this.editor.view; + const { to } = view.state.selection; + const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content); + + if (contentNode) { + const transaction = view.state.tr.insert(to, contentNode); + view.dispatch(transaction); + this.onValueChanged(this.editor.value); + } + } + + /** + * Replaces the currently selected text with new content + * @param content - HTML string to replace selection with + * @param asHtml - If true, parses as HTML; if false, inserts as plain text + */ + public replaceSelectedText(content: string, asHtml: boolean = false): void { + if (!this.editor || !this.editor.view) return; + + const view = this.editor.view; + const { from, to } = view.state.selection; + const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content); + + if (!contentNode) return; + + // If there's no selection, just insert at cursor position + if (from === to) { + const transaction = view.state.tr.insert(from, contentNode); + view.dispatch(transaction); + } else { + // Replace the selected content + const transaction = view.state.tr.replaceWith(from, to, contentNode); + view.dispatch(transaction); + } + this.onValueChanged(this.editor.value); + } + + /** + * Gets the currently selected text in the editor + * @returns The selected text as a string + */ + public getSelectedText(): string { + if (!this.editor) return ''; + return this.editor.selectionText || ''; + } + public getSelectionTextOrWholeText(): string { + if (!this.editor) return ''; + + // If there's selected text, return it + if (this.editor.selectionText && this.editor.selectionText.trim().length > 0) { + return this.editor.selectionText; + } + + // Otherwise, get the whole text content from the editor's document + if (this.editor.view && this.editor.view.state) { + return this.editor.view.state.doc.textContent; + } + + // Fallback: strip HTML tags and return plain text + const div = document.createElement('div'); + div.innerHTML = this.editor.value; + return div.textContent || div.innerText || ''; + } + + /** + * Gets the current cursor position or selection range + * @returns Object with 'from' and 'to' positions + */ + public getSelectionRange(): { from: number; to: number } | null { + if (!this.editor || !this.editor.view) return null; + + const { from, to } = this.editor.view.state.selection; + return { from, to }; + } + + /** + * Inserts HTML content at a specific position + * @param content - HTML string to insert + * @param position - Position to insert at (0 = beginning) + * @param asHtml - If true, parses as HTML; if false, inserts as plain text + */ + public insertAtPosition(content: string, position: number, asHtml: boolean = false): void { + if (!this.editor || !this.editor.view) return; + + const view = this.editor.view; + const maxPos = view.state.doc.content.size; + const safePos = Math.min(Math.max(0, position), maxPos); + const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content); + + if (contentNode) { + const transaction = view.state.tr.insert(safePos, contentNode); + view.dispatch(transaction); + this.onValueChanged(this.editor.value); + } + } + + /** + * Replaces text in a specific range + * @param content - HTML string to insert + * @param from - Start position + * @param to - End position + * @param asHtml - If true, parses as HTML; if false, inserts as plain text + */ + public replaceRange(content: string, from: number, to: number, asHtml: boolean = false): void { + if (!this.editor || !this.editor.view) return; + + const view = this.editor.view; + const maxPos = view.state.doc.content.size; + const safeFrom = Math.min(Math.max(0, from), maxPos); + const safeTo = Math.min(Math.max(safeFrom, to), maxPos); + const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content); + + if (contentNode) { + const transaction = view.state.tr.replaceWith(safeFrom, safeTo, contentNode); + view.dispatch(transaction); + this.onValueChanged(this.editor.value); + } + } + + + + showVariablePicker() { + this.easyEditorService.openTableMultiPicker(this.variables, this.variableTableSettings, "Please select a variable").pipe(first()).subscribe(result => { + if (result) { + result.forEach(c => { + this.insertAfterSelection(`<${RbjTagNode} class="rbj-tag" ${RbjTagIdAttribute}="${c.name}" ${RbjTagValueAttribute}="${c.name}">${c.name}`, true); + }); + } + }); + } + + + // Private methods + private createCustomSchema(): Schema { + let nodes = schema.spec.nodes.addBefore("div", "rbjTag", rbjTagNodeSpec); + let marks = schema.spec.marks; + //marks = marks.addToStart("rbjSpanTag", rbjTagMarkSpec); + return new Schema({ + nodes: nodes, + marks: marks + }); + } } + +// Define the custom node specification for the rbj-tag element. +export const rbjTagNodeSpec: NodeSpec = { + // Define the node attributes for the tag + attrs: { + "rbj-tag-id": { default: "" }, + "tag-value": { default: "" }, + "tag-preview": { default: "" } + }, + // Specify that this node should be treated as an inline element + inline: true, + // Allow the node to be part of inline content + group: "inline", + // Make it atomic (non-editable, treated as a single unit) + atom: true, + + // Define how the node should be rendered in the DOM + toDOM: (node) => { + let tagPreview = node.attrs["tag-preview"]; + let displayValue = tagPreview == 'true' ? node.attrs["tag-value"] : node.attrs["rbj-tag-id"]; + + return [ + "span", + { + class: "rbj-tag", + "rbj-tag-id": node.attrs["rbj-tag-id"], + "tag-marker": node.attrs["tag-marker"], + "tag-value": node.attrs["tag-value"], + "tag-preview": node.attrs["tag-preview"], + //contenteditable: "false", + //spellcheck: "false", + style: "display: inline;" + }, + //node.attrs["tag-marker"] + node.attrs["tag-value"] // Display the tag content directly + displayValue + ]; + }, + + // Define how to parse the node from existing DOM elements + parseDOM: [ + { + // Look for span elements with class rbj-tag (higher priority) + tag: "span[rbj-tag-id]", + priority: 51, // Higher priority to catch before other parsers + // Extract attributes from the DOM element + getAttrs: (dom) => { + const element = dom as HTMLElement; + // Must have rbj-tag-id attribute to be valid + if (!element.hasAttribute("rbj-tag-id")) { + return false; + } + return { + "rbj-tag-id": element.getAttribute("rbj-tag-id") || "", + "tag-preview": element.getAttribute("tag-preview") || "false", + "tag-value": element.getAttribute("tag-value") || element.getAttribute("rbj-tag-id") + }; + } + }, + { + // Look for div elements with rbj-tag-id attribute (for backward compatibility) + tag: "div[rbj-tag-id]", + // Extract attributes from the DOM element + getAttrs: (dom) => { + const element = dom as HTMLElement; + return { + "rbj-tag-id": element.getAttribute("rbj-tag-id") || "", + "tag-preview": element.getAttribute("tag-preview") || "false", + "tag-value": element.getAttribute("tag-value") || element.getAttribute("rbj-tag-id") + }; + } + } + ] +}; \ No newline at end of file