Update MD2 editor
This commit is contained in:
@@ -12,10 +12,10 @@
|
||||
</nb-accordion>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<md2-html-editor></md2-html-editor>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<md2-html-editor></md2-html-editor>
|
||||
|
||||
<div class="col-12 col-md-5">
|
||||
<nb-card>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { NumberUtils } from '../../utilities/number-utils';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MassiveDarkness2Component extends MD2Base implements OnInit {
|
||||
HeroClass: HeroClass
|
||||
HeroClass = HeroClass;
|
||||
constructor(
|
||||
private fileService: FileService,
|
||||
private initService: MD2InitService,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stringify } from "querystring"
|
||||
|
||||
import { Observable, Subject, Subscription } from "rxjs"
|
||||
import { first } from "rxjs/operators"
|
||||
import { MD2Service } from "../../services/MD2/md2.service"
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
<kendo-editor [value]="value" (valueChange)="onChange($event)" (blur)="onTouched()" [disabled]="disabled" [schema]="messageTemplateSchema"
|
||||
[iframeCss]="{ content: customCssClass }">
|
||||
<kendo-editor [value]="value" (valueChange)="onChange($event)" (blur)="onTouched()" [disabled]="disabled"
|
||||
[schema]="messageTemplateSchema" [iframe]="false">
|
||||
<kendo-toolbar>
|
||||
<!-- Standard editing tools -->
|
||||
<kendo-toolbar-button kendoEditorBold></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorItalic></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorUnderline></kendo-toolbar-button>
|
||||
<kendo-toolbar-separator></kendo-toolbar-separator>
|
||||
<kendo-toolbar-button kendoEditorUnorderedList></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorOrderedList></kendo-toolbar-button>
|
||||
<kendo-toolbar-separator></kendo-toolbar-separator>
|
||||
<kendo-toolbar-button kendoEditorUndo></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorRedo></kendo-toolbar-button>
|
||||
<kendo-toolbar-separator></kendo-toolbar-separator>
|
||||
<kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-button kendoEditorBoldButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorItalicButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorUnderlineButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorStrikethroughButton></kendo-toolbar-button>
|
||||
</kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-button kendoEditorSubscriptButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorSuperscriptButton></kendo-toolbar-button>
|
||||
</kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-button kendoEditorAlignLeftButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorAlignCenterButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorAlignRightButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorAlignJustifyButton></kendo-toolbar-button>
|
||||
</kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-dropdownlist kendoEditorFormat></kendo-toolbar-dropdownlist>
|
||||
<kendo-toolbar-dropdownlist kendoEditorFontSize #fontSizeDropdown
|
||||
[data]="fontSizeData"></kendo-toolbar-dropdownlist>
|
||||
<kendo-toolbar-dropdownlist kendoEditorFontFamily></kendo-toolbar-dropdownlist>
|
||||
<kendo-toolbar-colorpicker kendoEditorForeColor></kendo-toolbar-colorpicker>
|
||||
<kendo-toolbar-colorpicker kendoEditorBackColor view="gradient"></kendo-toolbar-colorpicker>
|
||||
<kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-button kendoEditorInsertUnorderedListButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorInsertOrderedListButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorIndentButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorOutdentButton></kendo-toolbar-button>
|
||||
</kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-button kendoEditorBlockquoteButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorSelectAllButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-button kendoEditorUndoButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorRedoButton></kendo-toolbar-button>
|
||||
</kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-button kendoEditorCreateLinkButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorUnlinkButton></kendo-toolbar-button>
|
||||
</kendo-toolbar-buttongroup>
|
||||
<kendo-toolbar-button kendoEditorInsertFileButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorInsertImageButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorViewSourceButton></kendo-toolbar-button>
|
||||
<kendo-toolbar-button kendoEditorCleanFormattingButton></kendo-toolbar-button>
|
||||
<!-- Custom MD2 Icon button -->
|
||||
<kendo-toolbar-button (click)="showInsertMD2Icon()">
|
||||
Insert Icon
|
||||
</kendo-toolbar-button>
|
||||
<kendo-toolbar-button text="Insert Icon" (click)="showInsertMD2Icon()"></kendo-toolbar-button>
|
||||
</kendo-toolbar>
|
||||
</kendo-editor>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, ElementRef, EventEmitter, Inject, Input, NgZone, Output, PLATFORM_ID, Renderer2, ViewChild } from '@angular/core';
|
||||
import { Component, ElementRef, EventEmitter, Inject, Input, NgZone, Output, PLATFORM_ID, Renderer2, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { ControlValueAccessor, Validator, AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
import { EditorComponent } from '@progress/kendo-angular-editor';
|
||||
import { EditorComponent, NodeSpec, schema, Schema, FontSizeItem } from '@progress/kendo-angular-editor';
|
||||
|
||||
import { MsgBoxService } from '../../../services/msg-box.service';
|
||||
import { DropDownOption } from '../../../entity/dropDownOption';
|
||||
@@ -9,21 +9,42 @@ import { first } from 'rxjs/operators';
|
||||
import { MD2StateService } from '../../../services/MD2/md2-state.service';
|
||||
import { MD2IconPickerDlgComponent } from './md2-icon-picker-dlg.component';
|
||||
import { NbDialogService } from '@nebular/theme';
|
||||
|
||||
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
|
||||
@Component({
|
||||
selector: 'md2-html-editor',
|
||||
templateUrl: './md2-html-editor.component.html',
|
||||
styleUrl: './md2-html-editor.component.scss'
|
||||
})
|
||||
export class MD2HtmlEditorComponent implements ControlValueAccessor {
|
||||
export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewInit {
|
||||
@ViewChild(EditorComponent) editor: EditorComponent;
|
||||
@ViewChild('fontSizeDropdown') fontSizeDropdown: any;
|
||||
|
||||
value: string = '';
|
||||
disabled: boolean = false;
|
||||
|
||||
messageTemplateSchema = this.createCustomSchema();
|
||||
messageTemplateSchema = this.createCustomSchema();
|
||||
customCssClass = '.MD2Icon{font-family: "Massive Darkness 2", sans-serif !important; font-size: 40px; margin-left:5px} body{font-size: 30px; }';
|
||||
|
||||
// Default font size: 30px
|
||||
fontSizeData: FontSizeItem[] = [
|
||||
{ size: 30, text: '30px' },
|
||||
{ size: 8, text: '8px' },
|
||||
{ size: 10, text: '10px' },
|
||||
{ size: 12, text: '12px' },
|
||||
{ size: 14, text: '14px' },
|
||||
{ size: 16, text: '16px' },
|
||||
{ size: 18, text: '18px' },
|
||||
{ size: 20, text: '20px' },
|
||||
{ size: 24, text: '24px' },
|
||||
{ size: 30, text: '30px' },
|
||||
{ size: 36, text: '36px' },
|
||||
{ size: 48, text: '48px' },
|
||||
{ size: 60, text: '60px' },
|
||||
{ size: 72, text: '72px' }
|
||||
];
|
||||
|
||||
defaultFontSize: FontSizeItem = { size: 30, text: '30px' };
|
||||
|
||||
// ControlValueAccessor interface
|
||||
private onChangeFn = (value: string) => { };
|
||||
private onTouchedFn = () => { };
|
||||
@@ -36,6 +57,23 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor {
|
||||
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Set default font size after view initialization
|
||||
// The fontSizeDropdown is the EditorFontSizeComponent instance
|
||||
setTimeout(() => {
|
||||
if (this.fontSizeDropdown) {
|
||||
// Access the fontSizeDropDownList component which has the defaultItem property
|
||||
if (this.fontSizeDropdown.fontSizeDropDownList) {
|
||||
this.fontSizeDropdown.fontSizeDropDownList.defaultItem = this.defaultFontSize;
|
||||
}
|
||||
// Also try setting it directly on the component if it has the property
|
||||
if (this.fontSizeDropdown.defaultItem !== undefined) {
|
||||
this.fontSizeDropdown.defaultItem = this.defaultFontSize;
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ControlValueAccessor implementation
|
||||
writeValue(value: string): void {
|
||||
this.value = value || '';
|
||||
@@ -68,6 +106,8 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor {
|
||||
closeOnEsc: true
|
||||
}).onClose.pipe(first()).subscribe((html: string) => {
|
||||
if (html && this.editor) {
|
||||
this.insertAfterSelection(html, true);
|
||||
return;
|
||||
// Insert the HTML content at the current cursor position
|
||||
// Use ProseMirror's dispatch method to insert content at cursor
|
||||
const view = this.editor.view;
|
||||
@@ -110,303 +150,296 @@ 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;
|
||||
// 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;
|
||||
|
||||
// Use ProseMirror's DOMParser to parse HTML into nodes
|
||||
const parser = ProseMirrorDOMParser.fromSchema(view.state.schema);
|
||||
const slice = parser.parseSlice(element);
|
||||
return slice.content;
|
||||
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.onChange(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.onChange(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.onChange(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.onChange(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.onChange(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// Fallback: strip HTML tags and return plain text
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = this.editor.value;
|
||||
return div.textContent || div.innerText || '';
|
||||
}
|
||||
|
||||
const view = this.editor.view;
|
||||
const endPos = view.state.doc.content.size;
|
||||
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (contentNode) {
|
||||
const transaction = view.state.tr.insert(endPos, contentNode);
|
||||
view.dispatch(transaction);
|
||||
this.onValueChanged(this.editor.value);
|
||||
}
|
||||
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.onChange(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;
|
||||
/**
|
||||
* 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 { from } = view.state.selection;
|
||||
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
|
||||
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.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);
|
||||
}
|
||||
if (contentNode) {
|
||||
const transaction = view.state.tr.replaceWith(safeFrom, safeTo, contentNode);
|
||||
view.dispatch(transaction);
|
||||
this.onChange(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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// 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
|
||||
});
|
||||
}
|
||||
// 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 the node attributes for the tag
|
||||
attrs: {
|
||||
"md2-icon": { default: "" },
|
||||
"class": { 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"];
|
||||
// Define how the node should be rendered in the DOM
|
||||
toDOM: (node) => {
|
||||
let md2IconText = node.attrs["md2-icon"] as string;
|
||||
let classValue = node.attrs["class"] as string;
|
||||
if (classValue.includes('dice')) {
|
||||
md2IconText = '';
|
||||
}
|
||||
// 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
|
||||
];
|
||||
},
|
||||
return [
|
||||
"span",
|
||||
{
|
||||
class: classValue,
|
||||
// "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="font-size: 36px;"
|
||||
style: "display: inline;"
|
||||
},
|
||||
//node.attrs["tag-marker"] + node.attrs["tag-value"] // Display the tag content directly
|
||||
md2IconText
|
||||
];
|
||||
},
|
||||
|
||||
// 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")
|
||||
};
|
||||
}
|
||||
// Define how to parse the node from existing DOM elements
|
||||
parseDOM: [
|
||||
{
|
||||
// Look for span elements with class rbj-tag (higher priority)
|
||||
tag: "span[md2-icon]",
|
||||
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("md2-icon")) {
|
||||
return false;
|
||||
}
|
||||
]
|
||||
return {
|
||||
"md2-icon": element.getAttribute("md2-icon") || "",
|
||||
"class": element.className || "",
|
||||
// "tag-preview": element.getAttribute("tag-preview") || "false",
|
||||
// "tag-value": element.getAttribute("tag-value") || element.getAttribute("rbj-tag-id")
|
||||
};
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -4,18 +4,18 @@ import { MD2Icon } from '../massive-darkness2.model';
|
||||
import { MD2StateService } from '../../../services/MD2/md2-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'md2-icon-picker-dlg',
|
||||
template: `
|
||||
selector: 'md2-icon-picker-dlg',
|
||||
template: `
|
||||
<nb-card style="max-width: 800px;">
|
||||
<nb-card-header>
|
||||
<h5>Insert MD2 Icon</h5>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<div class="icon-grid">
|
||||
<div class="md2-icon-grid">
|
||||
<div
|
||||
*ngFor="let iconData of iconList"
|
||||
(click)="selectIcon(iconData)"
|
||||
class="icon-item"
|
||||
class="icon-item" title="{{iconData.name}}"
|
||||
[innerHTML]="iconData.html">
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,13 +25,14 @@ import { MD2StateService } from '../../../services/MD2/md2-state.service';
|
||||
</nb-card-footer>
|
||||
</nb-card>
|
||||
`,
|
||||
styles: [`
|
||||
.icon-grid {
|
||||
styles: [`
|
||||
.md2-icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 10px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
width:400px;
|
||||
}
|
||||
.icon-item {
|
||||
cursor: pointer;
|
||||
@@ -42,40 +43,41 @@ import { MD2StateService } from '../../../services/MD2/md2-state.service';
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
font-size: 30px;
|
||||
}
|
||||
.icon-item:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #007bff;
|
||||
transform: scale(1.1);
|
||||
//transform: scale(1.1);
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class MD2IconPickerDlgComponent implements OnInit {
|
||||
|
||||
iconList: Array<{ icon: MD2Icon, name: string, html: string }> = [];
|
||||
iconList: Array<{ icon: MD2Icon, name: string, html: string }> = [];
|
||||
|
||||
constructor(
|
||||
private dlgRef: NbDialogRef<MD2IconPickerDlgComponent>,
|
||||
private md2StateService: MD2StateService
|
||||
) { }
|
||||
constructor(
|
||||
private dlgRef: NbDialogRef<MD2IconPickerDlgComponent>,
|
||||
private md2StateService: MD2StateService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Get all icons
|
||||
for (let icon of Object.values(MD2Icon).filter(val => typeof val === 'number') as MD2Icon[]) {
|
||||
this.iconList.push({
|
||||
icon: icon,
|
||||
name: MD2Icon[icon],
|
||||
html: this.md2StateService.iconHtml(icon)
|
||||
});
|
||||
}
|
||||
ngOnInit(): void {
|
||||
// Get all icons
|
||||
for (let icon of Object.values(MD2Icon).filter(val => typeof val === 'number') as MD2Icon[]) {
|
||||
this.iconList.push({
|
||||
icon: icon,
|
||||
name: MD2Icon[icon],
|
||||
html: this.md2StateService.iconHtml(icon)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
selectIcon(iconData: { icon: MD2Icon, name: string, html: string }) {
|
||||
this.dlgRef.close(iconData.html);
|
||||
}
|
||||
selectIcon(iconData: { icon: MD2Icon, name: string, html: string }) {
|
||||
this.dlgRef.close(iconData.html);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dlgRef.close(null);
|
||||
}
|
||||
cancel() {
|
||||
this.dlgRef.close(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NbDialogService } from '@nebular/theme';
|
||||
import { stringify } from 'querystring';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { DropDownOption } from '../../../entity/dropDownOption';
|
||||
import { FileService } from '../../../services/file.service';
|
||||
@@ -10,7 +9,6 @@ import { MD2Service } from '../../../services/MD2/md2.service';
|
||||
import { MsgBoxService } from '../../../services/msg-box.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
|
||||
import { ArrayUtils } from '../../../utilities/array-utils';
|
||||
import { NumberUtils } from '../../../utilities/number-utils';
|
||||
import { StringUtils } from '../../../utilities/string-utils';
|
||||
import { CoreGameMobFactories } from '../factorys/mobs/CoreGame';
|
||||
|
||||
Reference in New Issue
Block a user