Update src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts

This commit is contained in:
ChrisChen 2025-11-03 16:50:28 +00:00
parent ed90250876
commit ba3ad023ad

View File

@ -21,6 +21,7 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor {
value: string = ''; value: string = '';
disabled: boolean = false; 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; }'; customCssClass = '.MD2Icon{font-family: "Massive Darkness 2", sans-serif !important; font-size: 40px; margin-left:5px} body{font-size: 30px; }';
// ControlValueAccessor interface // 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}</${RbjTagNode}>`, 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")
};
}
}
]
};