UNPKG

@kolkov/angular-editor

Version:

A simple native WYSIWYG editor for Angular 20+. Rich Text editor component for Angular.

892 lines (885 loc) 89.6 kB
import * as i0 from '@angular/core'; import { DOCUMENT, Inject, Injectable, EventEmitter, forwardRef, HostListener, ViewChild, Output, HostBinding, Input, Component, SecurityContext, ContentChild, Attribute, ViewEncapsulation, NgModule } from '@angular/core'; import * as i1 from '@angular/common/http'; import * as i3 from '@angular/forms'; import { NG_VALUE_ACCESSOR, FormsModule, ReactiveFormsModule } from '@angular/forms'; import * as i2 from '@angular/platform-browser'; import * as i1$1 from '@angular/common'; import { CommonModule } from '@angular/common'; class AngularEditorService { http; doc; savedSelection; selectedText; uploadUrl; uploadWithCredentials; constructor(http, doc) { this.http = http; this.doc = doc; } /** * Executed command from editor header buttons exclude toggleEditorMode * @param command string from triggerCommand * @param value */ executeCommand(command, value) { const commands = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre']; if (commands.includes(command)) { this.doc.execCommand('formatBlock', false, command); return; } this.doc.execCommand(command, false, value); } /** * Create URL link * @param url string from UI prompt */ createLink(url) { if (!url.includes('http')) { this.doc.execCommand('createlink', false, url); } else { const newUrl = '<a href="' + url + '" target="_blank">' + this.selectedText + '</a>'; this.insertHtml(newUrl); } } /** * insert color either font or background * * @param color color to be inserted * @param where where the color has to be inserted either text/background */ insertColor(color, where) { const restored = this.restoreSelection(); if (restored) { if (where === 'textColor') { this.doc.execCommand('foreColor', false, color); } else { this.doc.execCommand('hiliteColor', false, color); } } } /** * Set font name * @param fontName string */ setFontName(fontName) { this.doc.execCommand('fontName', false, fontName); } /** * Set font size * @param fontSize string */ setFontSize(fontSize) { this.doc.execCommand('fontSize', false, fontSize); } /** * Create raw HTML * @param html HTML string */ insertHtml(html) { const isHTMLInserted = this.doc.execCommand('insertHTML', false, html); if (!isHTMLInserted) { throw new Error('Unable to perform the operation'); } } /** * save selection when the editor is focussed out */ saveSelection = () => { if (this.doc.getSelection) { const sel = this.doc.getSelection(); if (sel.getRangeAt && sel.rangeCount) { this.savedSelection = sel.getRangeAt(0); this.selectedText = sel.toString(); } } else if (this.doc.getSelection && this.doc.createRange) { this.savedSelection = document.createRange(); } else { this.savedSelection = null; } }; /** * restore selection when the editor is focused in * * saved selection when the editor is focused out */ restoreSelection() { if (this.savedSelection) { if (this.doc.getSelection) { const sel = this.doc.getSelection(); sel.removeAllRanges(); sel.addRange(this.savedSelection); return true; } else if (this.doc.getSelection /*&& this.savedSelection.select*/) { // this.savedSelection.select(); return true; } } else { return false; } } /** * setTimeout used for execute 'saveSelection' method in next event loop iteration */ executeInNextQueueIteration(callbackFn, timeout = 1e2) { setTimeout(callbackFn, timeout); } /** check any selection is made or not */ checkSelection() { const selectedText = this.savedSelection.toString(); if (selectedText.length === 0) { throw new Error('No Selection Made'); } return true; } /** * Upload file to uploadUrl * @param file The file */ uploadImage(file) { const uploadData = new FormData(); uploadData.append('file', file, file.name); return this.http.post(this.uploadUrl, uploadData, { reportProgress: true, observe: 'events', withCredentials: this.uploadWithCredentials, }); } /** * Insert image with Url * @param imageUrl The imageUrl. */ insertImage(imageUrl) { this.doc.execCommand('insertImage', false, imageUrl); } setDefaultParagraphSeparator(separator) { this.doc.execCommand('defaultParagraphSeparator', false, separator); } createCustomClass(customClass) { let newTag = this.selectedText; if (customClass) { const tagName = customClass.tag ? customClass.tag : 'span'; newTag = '<' + tagName + ' class="' + customClass.class + '">' + this.selectedText + '</' + tagName + '>'; } this.insertHtml(newTag); } insertVideo(videoUrl) { if (videoUrl.match('www.youtube.com') || videoUrl.match('youtu.be')) { this.insertYouTubeVideoTag(videoUrl); } if (videoUrl.match('vimeo.com')) { this.insertVimeoVideoTag(videoUrl); } } insertYouTubeVideoTag(videoUrl) { // Support both formats: youtube.com/watch?v=ID and youtu.be/ID let id; if (videoUrl.includes('youtu.be/')) { id = videoUrl.split('youtu.be/')[1].split('?')[0]; } else { id = videoUrl.split('v=')[1].split('&')[0]; } const imageUrl = `https://img.youtube.com/vi/${id}/0.jpg`; const thumbnail = ` <div style='position: relative'> <a href='${videoUrl}' target='_blank'> <img src="${imageUrl}" alt="click to watch"/> <img style='position: absolute; left:200px; top:140px' src="https://img.icons8.com/color/96/000000/youtube-play.png"/> </a> </div>`; this.insertHtml(thumbnail); } insertVimeoVideoTag(videoUrl) { const sub = this.http.get(`https://vimeo.com/api/oembed.json?url=${videoUrl}`).subscribe(data => { const imageUrl = data.thumbnail_url_with_play_button; const thumbnail = `<div> <a href='${videoUrl}' target='_blank'> <img src="${imageUrl}" alt="${data.title}"/> </a> </div>`; this.insertHtml(thumbnail); sub.unsubscribe(); }); } nextNode(node) { if (node.hasChildNodes()) { return node.firstChild; } else { while (node && !node.nextSibling) { node = node.parentNode; } if (!node) { return null; } return node.nextSibling; } } getRangeSelectedNodes(range, includePartiallySelectedContainers) { let node = range.startContainer; const endNode = range.endContainer; let rangeNodes = []; // Special case for a range that is contained within a single node if (node === endNode) { rangeNodes = [node]; } else { // Iterate nodes until we hit the end container while (node && node !== endNode) { rangeNodes.push(node = this.nextNode(node)); } // Add partially selected nodes at the start of the range node = range.startContainer; while (node && node !== range.commonAncestorContainer) { rangeNodes.unshift(node); node = node.parentNode; } } // Add ancestors of the range container, if required if (includePartiallySelectedContainers) { node = range.commonAncestorContainer; while (node) { rangeNodes.push(node); node = node.parentNode; } } return rangeNodes; } getSelectedNodes() { const nodes = []; if (this.doc.getSelection) { const sel = this.doc.getSelection(); for (let i = 0, len = sel.rangeCount; i < len; ++i) { nodes.push.apply(nodes, this.getRangeSelectedNodes(sel.getRangeAt(i), true)); } } return nodes; } replaceWithOwnChildren(el) { const parent = el.parentNode; while (el.hasChildNodes()) { parent.insertBefore(el.firstChild, el); } parent.removeChild(el); } removeSelectedElements(tagNames) { const tagNamesArray = tagNames.toLowerCase().split(','); this.getSelectedNodes().forEach((node) => { if (node.nodeType === 1 && tagNamesArray.indexOf(node.tagName.toLowerCase()) > -1) { // Remove the node and replace it with its children this.replaceWithOwnChildren(node); } }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AngularEditorService, deps: [{ token: i1.HttpClient }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AngularEditorService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AngularEditorService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }] }); const angularEditorConfig = { editable: true, spellcheck: true, height: 'auto', minHeight: '0', maxHeight: 'auto', width: 'auto', minWidth: '0', translate: 'yes', enableToolbar: true, showToolbar: true, placeholder: 'Enter text here...', defaultParagraphSeparator: '', defaultFontName: '', defaultFontSize: '', fonts: [ { class: 'arial', name: 'Arial' }, { class: 'times-new-roman', name: 'Times New Roman' }, { class: 'calibri', name: 'Calibri' }, { class: 'comic-sans-ms', name: 'Comic Sans MS' } ], uploadUrl: 'v1/image', uploadWithCredentials: false, sanitize: true, toolbarPosition: 'top', outline: true, /*toolbarHiddenButtons: [ ['bold', 'italic', 'underline', 'strikeThrough', 'superscript', 'subscript'], ['heading', 'fontName', 'fontSize', 'color'], ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'indent', 'outdent'], ['cut', 'copy', 'delete', 'removeFormat', 'undo', 'redo'], ['paragraph', 'blockquote', 'removeBlockquote', 'horizontalLine', 'orderedList', 'unorderedList'], ['link', 'unlink', 'image', 'video'] ]*/ }; function isDefined(value) { return value !== undefined && value !== null; } class AeSelectComponent { elRef; r; options = []; // eslint-disable-next-line @angular-eslint/no-input-rename isHidden; selectedOption; disabled = false; optionId = 0; get label() { return this.selectedOption && this.selectedOption.hasOwnProperty('label') ? this.selectedOption.label : 'Select'; } opened = false; get value() { return this.selectedOption.value; } hidden = 'inline-block'; // eslint-disable-next-line @angular-eslint/no-output-native, @angular-eslint/no-output-rename changeEvent = new EventEmitter(); labelButton; constructor(elRef, r) { this.elRef = elRef; this.r = r; } ngOnInit() { this.selectedOption = this.options[0]; if (isDefined(this.isHidden) && this.isHidden) { this.hide(); } } hide() { this.hidden = 'none'; } optionSelect(option, event) { //console.log(event.button, event.buttons); if (event.buttons !== 1) { return; } event.preventDefault(); event.stopPropagation(); this.setValue(option.value); this.onChange(this.selectedOption.value); this.changeEvent.emit(this.selectedOption.value); this.onTouched(); this.opened = false; } toggleOpen(event) { // event.stopPropagation(); if (this.disabled) { return; } this.opened = !this.opened; } onClick($event) { if (!this.elRef.nativeElement.contains($event.target)) { this.close(); } } close() { this.opened = false; } get isOpen() { return this.opened; } writeValue(value) { if (!value || typeof value !== 'string') { return; } this.setValue(value); } setValue(value) { let index = 0; const selectedEl = this.options.find((el, i) => { index = i; return el.value === value; }); if (selectedEl) { this.selectedOption = selectedEl; this.optionId = index; } } onChange = () => { }; onTouched = () => { }; registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { this.onTouched = fn; } setDisabledState(isDisabled) { this.labelButton.nativeElement.disabled = isDisabled; const div = this.labelButton.nativeElement; const action = isDisabled ? 'addClass' : 'removeClass'; this.r[action](div, 'disabled'); this.disabled = isDisabled; } handleKeyDown($event) { if (!this.opened) { return; } // console.log($event.key); // if (KeyCode[$event.key]) { switch ($event.key) { case 'ArrowDown': this._handleArrowDown($event); break; case 'ArrowUp': this._handleArrowUp($event); break; case 'Space': this._handleSpace($event); break; case 'Enter': this._handleEnter($event); break; case 'Tab': this._handleTab($event); break; case 'Escape': this.close(); $event.preventDefault(); break; case 'Backspace': this._handleBackspace(); break; } // } else if ($event.key && $event.key.length === 1) { // this._keyPress$.next($event.key.toLocaleLowerCase()); // } } _handleArrowDown($event) { if (this.optionId < this.options.length - 1) { this.optionId++; } } _handleArrowUp($event) { if (this.optionId >= 1) { this.optionId--; } } _handleSpace($event) { } _handleEnter($event) { this.optionSelect(this.options[this.optionId], $event); } _handleTab($event) { } _handleBackspace() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AeSelectComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: AeSelectComponent, isStandalone: false, selector: "ae-select", inputs: { options: "options", isHidden: ["hidden", "isHidden"] }, outputs: { changeEvent: "change" }, host: { listeners: { "document:click": "onClick($event)", "keydown": "handleKeyDown($event)" }, properties: { "style.display": "this.hidden" } }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AeSelectComponent), multi: true, } ], viewQueries: [{ propertyName: "labelButton", first: true, predicate: ["labelButton"], descendants: true, static: true }], ngImport: i0, template: "<span class=\"ae-picker\" [ngClass]=\"{'ae-expanded':isOpen}\">\n <button [tabIndex]=\"-1\" #labelButton tabindex=\"-1\" type=\"button\" role=\"button\" class=\"ae-picker-label\" (click)=\"toggleOpen($event);\">{{label}}\n <svg>\n <use [attr.href]=\"'assets/ae-icons/icons.svg#sort'\" [attr.xlink:href]=\"'assets/ae-icons/icons.svg#sort'\"></use>\n </svg>\n </button>\n <span class=\"ae-picker-options\">\n <span tabindex=\"-1\" type=\"button\" role=\"button\" class=\"ae-picker-item\"\n *ngFor=\"let item of options; let i = index\"\n [ngClass]=\"{'selected': item.value === value, 'focused': i === optionId}\"\n (mousedown)=\"optionSelect(item, $event)\">\n {{item.label}}\n </span>\n <span class=\"dropdown-item\" *ngIf=\"!options.length\">No items for select</span>\n </span>\n</span>\n", styles: ["a{cursor:pointer}svg{width:100%;height:100%}.ae-picker{color:var(--ae-picker-color, #444);display:inline-block;float:left;width:100%;position:relative;vertical-align:middle}.ae-picker-label{cursor:pointer;display:inline-block;padding-left:8px;padding-right:10px;position:relative;width:100%;line-height:1.8rem;vertical-align:middle;font-size:85%;text-align:left;background-color:var(--ae-picker-label-color, white);min-width:2rem;float:left;border:1px solid #ddd;border-radius:var(--ae-button-radius, 4px);text-overflow:clip;overflow:hidden;white-space:nowrap;height:2rem}.ae-picker-label:before{content:\"\";position:absolute;right:0;top:0;width:20px;height:100%;background:linear-gradient(to right,var(--ae-picker-label-color, white),var(--ae-picker-label-color, white) 100%)}.ae-picker-label:focus{outline:none}.ae-picker-label:hover{cursor:pointer;background-color:#f1f1f1;transition:.2s ease}.ae-picker-label:hover:before{background:linear-gradient(to right,#f5f5f5 100%,#fff)}.ae-picker-label:disabled{background-color:#f5f5f5;pointer-events:none;cursor:not-allowed}.ae-picker-label:disabled:before{background:linear-gradient(to right,#f5f5f5 100%,#fff)}.ae-picker-label svg{position:absolute;right:0;width:1rem}.ae-picker-label svg:not(:root){overflow:hidden}.ae-picker-label svg .ae-stroke{fill:none;stroke:#444;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.ae-picker-options{background-color:var(--ae-picker-option-bg-color, #fff);display:none;min-width:100%;position:absolute;white-space:nowrap;z-index:3;border:1px solid transparent;box-shadow:#0003 0 2px 8px}.ae-picker-options .ae-picker-item{cursor:pointer;display:block;padding:5px;z-index:3;text-align:left;background-color:transparent;min-width:2rem;border:0 solid #ddd}.ae-picker-options .ae-picker-item.selected{color:#06c;background-color:var(--ae-picker-option-active-bg-color, #fff4c2)}.ae-picker-options .ae-picker-item.focused{background-color:var(--ae-picker-option-focused-bg-color, #fbf9b0)}.ae-picker-options .ae-picker-item:hover{background-color:var(--ae-picker-option-hover-bg-color, #fffa98)}.ae-expanded{display:block;margin-top:-1px;z-index:1}.ae-expanded .ae-picker-label{color:#ccc;z-index:2}.ae-expanded .ae-picker-label svg{color:#ccc;z-index:2}.ae-expanded .ae-picker-label svg .ae-stroke{stroke:#ccc}.ae-expanded .ae-picker-options{display:flex;flex-direction:column;margin-top:-1px;top:100%;z-index:3;border-color:#ccc}\n"], dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AeSelectComponent, decorators: [{ type: Component, args: [{ selector: 'ae-select', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AeSelectComponent), multi: true, } ], standalone: false, template: "<span class=\"ae-picker\" [ngClass]=\"{'ae-expanded':isOpen}\">\n <button [tabIndex]=\"-1\" #labelButton tabindex=\"-1\" type=\"button\" role=\"button\" class=\"ae-picker-label\" (click)=\"toggleOpen($event);\">{{label}}\n <svg>\n <use [attr.href]=\"'assets/ae-icons/icons.svg#sort'\" [attr.xlink:href]=\"'assets/ae-icons/icons.svg#sort'\"></use>\n </svg>\n </button>\n <span class=\"ae-picker-options\">\n <span tabindex=\"-1\" type=\"button\" role=\"button\" class=\"ae-picker-item\"\n *ngFor=\"let item of options; let i = index\"\n [ngClass]=\"{'selected': item.value === value, 'focused': i === optionId}\"\n (mousedown)=\"optionSelect(item, $event)\">\n {{item.label}}\n </span>\n <span class=\"dropdown-item\" *ngIf=\"!options.length\">No items for select</span>\n </span>\n</span>\n", styles: ["a{cursor:pointer}svg{width:100%;height:100%}.ae-picker{color:var(--ae-picker-color, #444);display:inline-block;float:left;width:100%;position:relative;vertical-align:middle}.ae-picker-label{cursor:pointer;display:inline-block;padding-left:8px;padding-right:10px;position:relative;width:100%;line-height:1.8rem;vertical-align:middle;font-size:85%;text-align:left;background-color:var(--ae-picker-label-color, white);min-width:2rem;float:left;border:1px solid #ddd;border-radius:var(--ae-button-radius, 4px);text-overflow:clip;overflow:hidden;white-space:nowrap;height:2rem}.ae-picker-label:before{content:\"\";position:absolute;right:0;top:0;width:20px;height:100%;background:linear-gradient(to right,var(--ae-picker-label-color, white),var(--ae-picker-label-color, white) 100%)}.ae-picker-label:focus{outline:none}.ae-picker-label:hover{cursor:pointer;background-color:#f1f1f1;transition:.2s ease}.ae-picker-label:hover:before{background:linear-gradient(to right,#f5f5f5 100%,#fff)}.ae-picker-label:disabled{background-color:#f5f5f5;pointer-events:none;cursor:not-allowed}.ae-picker-label:disabled:before{background:linear-gradient(to right,#f5f5f5 100%,#fff)}.ae-picker-label svg{position:absolute;right:0;width:1rem}.ae-picker-label svg:not(:root){overflow:hidden}.ae-picker-label svg .ae-stroke{fill:none;stroke:#444;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.ae-picker-options{background-color:var(--ae-picker-option-bg-color, #fff);display:none;min-width:100%;position:absolute;white-space:nowrap;z-index:3;border:1px solid transparent;box-shadow:#0003 0 2px 8px}.ae-picker-options .ae-picker-item{cursor:pointer;display:block;padding:5px;z-index:3;text-align:left;background-color:transparent;min-width:2rem;border:0 solid #ddd}.ae-picker-options .ae-picker-item.selected{color:#06c;background-color:var(--ae-picker-option-active-bg-color, #fff4c2)}.ae-picker-options .ae-picker-item.focused{background-color:var(--ae-picker-option-focused-bg-color, #fbf9b0)}.ae-picker-options .ae-picker-item:hover{background-color:var(--ae-picker-option-hover-bg-color, #fffa98)}.ae-expanded{display:block;margin-top:-1px;z-index:1}.ae-expanded .ae-picker-label{color:#ccc;z-index:2}.ae-expanded .ae-picker-label svg{color:#ccc;z-index:2}.ae-expanded .ae-picker-label svg .ae-stroke{stroke:#ccc}.ae-expanded .ae-picker-options{display:flex;flex-direction:column;margin-top:-1px;top:100%;z-index:3;border-color:#ccc}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { options: [{ type: Input }], isHidden: [{ type: Input, args: ['hidden'] }], hidden: [{ type: HostBinding, args: ['style.display'] }], changeEvent: [{ type: Output, args: ['change'] }], labelButton: [{ type: ViewChild, args: ['labelButton', { static: true }] }], onClick: [{ type: HostListener, args: ['document:click', ['$event']] }], handleKeyDown: [{ type: HostListener, args: ['keydown', ['$event']] }] } }); class AeButtonComponent { iconName = ''; constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AeButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: AeButtonComponent, isStandalone: false, selector: "ae-button, button[aeButton]", inputs: { iconName: "iconName" }, host: { properties: { "tabIndex": "-1", "type": "\"button\"" }, classAttribute: "angular-editor-button" }, ngImport: i0, template: "<ng-container *ngIf=\"iconName; else contentTemplate\">\n <svg>\n <use [attr.href]=\"'assets/ae-icons/icons.svg#' + iconName\" [attr.xlink:href]=\"'assets/ae-icons/icons.svg#' + iconName\"></use>\n </svg>\n</ng-container>\n<ng-template #contentTemplate>\n <ng-content></ng-content>\n</ng-template>\n", styles: ["a{cursor:pointer}:host.angular-editor-button{background-color:var(--ae-button-bg-color, white);vertical-align:middle;border:var(--ae-button-border, 1px solid #ddd);border-radius:var(--ae-button-radius, 4px);padding:.4rem;float:left;width:2rem;height:2rem}:host.angular-editor-button svg{width:100%;height:100%}:host.angular-editor-button:hover{cursor:pointer;background-color:var(--ae-button-hover-bg-color, #f1f1f1);transition:.2s ease}:host.angular-editor-button:focus,:host.angular-editor-button.focus{outline:0}:host.angular-editor-button:disabled{background-color:var(--ae-button-disabled-bg-color, #f5f5f5);pointer-events:none;cursor:not-allowed}:host.angular-editor-button:disabled>.color-label{pointer-events:none;cursor:not-allowed}:host.angular-editor-button:disabled>.color-label.foreground :after{background:#555}:host.angular-editor-button:disabled>.color-label.background{background:#555}:host.angular-editor-button.active{background:var(--ae-button-active-bg-color, #fffbd3)}:host.angular-editor-button.active:hover{background-color:var(--ae-button-active-hover-bg-color, #fffaad)}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AeButtonComponent, decorators: [{ type: Component, args: [{ selector: 'ae-button, button[aeButton]', host: { 'class': 'angular-editor-button', '[tabIndex]': '-1', '[type]': '"button"', }, standalone: false, template: "<ng-container *ngIf=\"iconName; else contentTemplate\">\n <svg>\n <use [attr.href]=\"'assets/ae-icons/icons.svg#' + iconName\" [attr.xlink:href]=\"'assets/ae-icons/icons.svg#' + iconName\"></use>\n </svg>\n</ng-container>\n<ng-template #contentTemplate>\n <ng-content></ng-content>\n</ng-template>\n", styles: ["a{cursor:pointer}:host.angular-editor-button{background-color:var(--ae-button-bg-color, white);vertical-align:middle;border:var(--ae-button-border, 1px solid #ddd);border-radius:var(--ae-button-radius, 4px);padding:.4rem;float:left;width:2rem;height:2rem}:host.angular-editor-button svg{width:100%;height:100%}:host.angular-editor-button:hover{cursor:pointer;background-color:var(--ae-button-hover-bg-color, #f1f1f1);transition:.2s ease}:host.angular-editor-button:focus,:host.angular-editor-button.focus{outline:0}:host.angular-editor-button:disabled{background-color:var(--ae-button-disabled-bg-color, #f5f5f5);pointer-events:none;cursor:not-allowed}:host.angular-editor-button:disabled>.color-label{pointer-events:none;cursor:not-allowed}:host.angular-editor-button:disabled>.color-label.foreground :after{background:#555}:host.angular-editor-button:disabled>.color-label.background{background:#555}:host.angular-editor-button.active{background:var(--ae-button-active-bg-color, #fffbd3)}:host.angular-editor-button.active:hover{background-color:var(--ae-button-active-hover-bg-color, #fffaad)}\n"] }] }], ctorParameters: () => [], propDecorators: { iconName: [{ type: Input }] } }); class AeToolbarSetComponent { constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AeToolbarSetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: AeToolbarSetComponent, isStandalone: false, selector: "ae-toolbar-set, [aeToolbarSet]", host: { classAttribute: "angular-editor-toolbar-set" }, ngImport: i0, template: "<ng-content></ng-content>\n", styles: ["a{cursor:pointer}:host.angular-editor-toolbar-set{display:flex;gap:1px;width:fit-content;vertical-align:baseline}\n"] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AeToolbarSetComponent, decorators: [{ type: Component, args: [{ selector: 'ae-toolbar-set, [aeToolbarSet]', host: { 'class': 'angular-editor-toolbar-set' }, standalone: false, template: "<ng-content></ng-content>\n", styles: ["a{cursor:pointer}:host.angular-editor-toolbar-set{display:flex;gap:1px;width:fit-content;vertical-align:baseline}\n"] }] }], ctorParameters: () => [] }); class AeToolbarComponent { r; editorService; er; doc; htmlMode = false; linkSelected = false; block = 'default'; fontName = 'Times New Roman'; fontSize = '3'; foreColour; backColor; headings = [ { label: 'Heading 1', value: 'h1', }, { label: 'Heading 2', value: 'h2', }, { label: 'Heading 3', value: 'h3', }, { label: 'Heading 4', value: 'h4', }, { label: 'Heading 5', value: 'h5', }, { label: 'Heading 6', value: 'h6', }, { label: 'Paragraph', value: 'p', }, { label: 'Predefined', value: 'pre' }, { label: 'Standard', value: 'div' }, { label: 'default', value: 'default' } ]; fontSizes = [ { label: '1', value: '1', }, { label: '2', value: '2', }, { label: '3', value: '3', }, { label: '4', value: '4', }, { label: '5', value: '5', }, { label: '6', value: '6', }, { label: '7', value: '7', } ]; customClassId = '-1'; // eslint-disable-next-line no-underscore-dangle, id-blacklist, id-match _customClasses; customClassList = [{ label: '', value: '' }]; // uploadUrl: string; tagMap = { BLOCKQUOTE: 'indent', A: 'link' }; select = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'PRE', 'DIV']; buttons = ['bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'indent', 'outdent', 'insertUnorderedList', 'insertOrderedList', 'link']; id; uploadUrl; upload; showToolbar; fonts = [{ label: '', value: '' }]; set customClasses(classes) { if (classes) { this._customClasses = classes; this.customClassList = this._customClasses.map((x, i) => ({ label: x.name, value: i.toString() })); this.customClassList.unshift({ label: 'Clear Class', value: '-1' }); } } set defaultFontName(value) { if (value) { this.fontName = value; } } set defaultFontSize(value) { if (value) { this.fontSize = value; } } hiddenButtons; execute = new EventEmitter(); myInputFile; get isLinkButtonDisabled() { return this.htmlMode || !Boolean(this.editorService.selectedText); } constructor(r, editorService, er, doc) { this.r = r; this.editorService = editorService; this.er = er; this.doc = doc; } /** * Trigger command from editor header buttons * @param command string from toolbar buttons */ triggerCommand(command) { this.execute.emit(command); } /** * highlight editor buttons when cursor moved or positioning */ triggerButtons() { if (!this.showToolbar) { return; } this.buttons.forEach(e => { const result = this.doc.queryCommandState(e); const elementById = this.doc.getElementById(e + '-' + this.id); if (result) { this.r.addClass(elementById, 'active'); } else { this.r.removeClass(elementById, 'active'); } }); } /** * trigger highlight editor buttons when cursor moved or positioning in block */ triggerBlocks(nodes) { if (!this.showToolbar) { return; } this.linkSelected = nodes.findIndex(x => x.nodeName === 'A') > -1; let found = false; this.select.forEach(y => { const node = nodes.find(x => x.nodeName === y); if (node !== undefined && y === node.nodeName) { if (found === false) { this.block = node.nodeName.toLowerCase(); found = true; } } else if (found === false) { this.block = 'default'; } }); found = false; if (this._customClasses) { this._customClasses.forEach((y, index) => { const node = nodes.find(x => { if (x instanceof Element) { return x.className === y.class; } }); if (node !== undefined) { if (found === false) { this.customClassId = index.toString(); found = true; } } else if (found === false) { this.customClassId = '-1'; } }); } Object.keys(this.tagMap).map(e => { const elementById = this.doc.getElementById(this.tagMap[e] + '-' + this.id); const node = nodes.find(x => x.nodeName === e); if (node !== undefined && e === node.nodeName) { this.r.addClass(elementById, 'active'); } else { this.r.removeClass(elementById, 'active'); } }); this.foreColour = this.doc.queryCommandValue('ForeColor'); this.fontSize = this.doc.queryCommandValue('FontSize'); this.fontName = this.doc.queryCommandValue('FontName').replace(/"/g, ''); this.backColor = this.doc.queryCommandValue('backColor'); } /** * insert URL link */ insertUrl() { let url = 'https:\/\/'; const selection = this.editorService.savedSelection; if (selection && selection.commonAncestorContainer.parentElement.nodeName === 'A') { const parent = selection.commonAncestorContainer.parentElement; // Use getAttribute to preserve relative URLs instead of href which returns absolute URL const href = parent.getAttribute('href'); if (href !== '' && href !== null) { url = href; } } url = prompt('Insert URL link', url); if (url && url !== '' && url !== 'https://') { this.editorService.createLink(url); } } /** * insert Video link */ insertVideo() { this.execute.emit(''); const url = prompt('Insert Video link', `https://`); if (url && url !== '' && url !== `https://`) { this.editorService.insertVideo(url); } } /** insert color */ insertColor(color, where) { this.editorService.insertColor(color, where); this.execute.emit(''); } /** * set font Name/family * @param foreColor string */ setFontName(foreColor) { this.editorService.setFontName(foreColor); this.execute.emit(''); } /** * set font Size * @param fontSize string */ setFontSize(fontSize) { this.editorService.setFontSize(fontSize); this.execute.emit(''); } /** * toggle editor mode (WYSIWYG or SOURCE) * @param m boolean */ setEditorMode(m) { const toggleEditorModeButton = this.doc.getElementById('toggleEditorMode' + '-' + this.id); if (m) { this.r.addClass(toggleEditorModeButton, 'active'); } else { this.r.removeClass(toggleEditorModeButton, 'active'); } this.htmlMode = m; } /** * Upload image when file is selected. */ onFileChanged(event) { const file = event.target.files[0]; if (file.type.includes('image/')) { if (this.upload) { this.upload(file).subscribe((response) => this.watchUploadImage(response, event)); } else if (this.uploadUrl) { this.editorService.uploadImage(file).subscribe((response) => this.watchUploadImage(response, event)); } else { const reader = new FileReader(); reader.onload = (e) => { const fr = e.currentTarget; this.editorService.insertImage(fr.result.toString()); // Reset input value to allow re-uploading the same file event.target.value = null; }; reader.readAsDataURL(file); } } } watchUploadImage(response, event) { const { imageUrl } = response.body; this.editorService.insertImage(imageUrl); event.srcElement.value = null; } /** * Set custom class */ setCustomClass(classId) { if (classId === '-1') { this.execute.emit('clear'); } else { this.editorService.createCustomClass(this._customClasses[+classId]); } } isButtonHidden(name) { if (!name) { return false; } if (!(this.hiddenButtons instanceof Array)) { return false; } let result; for (const arr of this.hiddenButtons) { if (arr instanceof Array) { result = arr.find(item => item === name); } if (result) { break; } } return result !== undefined; } focus() { this.execute.emit('focus'); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AeToolbarComponent, deps: [{ token: i0.Renderer2 }, { token: AngularEditorService }, { token: i0.ElementRef }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: AeToolbarComponent, isStandalone: false, selector: "angular-editor-toolbar, ae-toolbar, div[aeToolbar]", inputs: { id: "id", uploadUrl: "uploadUrl", upload: "upload", showToolbar: "showToolbar", fonts: "fonts", customClasses: "customClasses", defaultFontName: "defaultFontName", defaultFontSize: "defaultFontSize", hiddenButtons: "hiddenButtons" }, outputs: { execute: "execute" }, viewQueries: [{ propertyName: "myInputFile", first: true, predicate: ["fileInput"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"angular-editor-toolbar\" *ngIf=\"showToolbar\">\n <div aeToolbarSet>\n <button aeButton title=\"Undo\" iconName=\"undo\" (click)=\"triggerCommand('undo')\" [hidden]=\"isButtonHidden('undo')\">\n </button>\n <button aeButton title=\"Redo\" iconName=\"redo\" (click)=\"triggerCommand('redo')\"\n [hidden]=\"isButtonHidden('redo')\">\n </button>\n </div>\n <div aeToolbarSet>\n <button [id]=\"'bold-'+id\" aeButton title=\"Bold\" iconName=\"bold\" (click)=\"triggerCommand('bold')\"\n [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('bold')\">\n </button>\n <button [id]=\"'italic-'+id\" aeButton iconName=\"italic\" title=\"Italic\" (click)=\"triggerCommand('italic')\"\n [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('italic')\">\n </button>\n <button [id]=\"'underline-'+id\" aeButton title=\"Underline\" iconName=\"underline\"\n (click)=\"triggerCommand('underline')\" [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('underline')\">\n </button>\n <button [id]=\"'strikeThrough-'+id\" aeButton iconName=\"strikeThrough\" title=\"Strikethrough\"\n (click)=\"triggerCommand('strikeThrough')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('strikeThrough')\">\n </button>\n <button [id]=\"'subscript-'+id\" aeButton title=\"Subscript\" iconName=\"subscript\"\n (click)=\"triggerCommand('subscript')\" [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('subscript')\">\n </button>\n <button [id]=\"'superscript-'+id\" aeButton iconName=\"superscript\" title=\"Superscript\"\n (click)=\"triggerCommand('superscript')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('superscript')\">\n </button>\n </div>\n <div aeToolbarSet>\n <button [id]=\"'justifyLeft-'+id\" aeButton iconName=\"justifyLeft\" title=\"Justify Left\"\n (click)=\"triggerCommand('justifyLeft')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('justifyLeft')\">\n </button>\n <button [id]=\"'justifyCenter-'+id\" aeButton iconName=\"justifyCenter\" title=\"Justify Center\"\n (click)=\"triggerCommand('justifyCenter')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('justifyCenter')\">\n </button>\n <button [id]=\"'justifyRight-'+id\" aeButton iconName=\"justifyRight\" title=\"Justify Right\"\n (click)=\"triggerCommand('justifyRight')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('justifyRight')\">\n </button>\n <button [id]=\"'justifyFull-'+id\" aeButton iconName=\"justifyFull\" title=\"Justify Full\"\n (click)=\"triggerCommand('justifyFull')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('justifyFull')\">\n </button>\n </div>\n <div aeToolbarSet>\n <button [id]=\"'indent-'+id\" aeButton iconName=\"indent\" title=\"Indent\"\n (click)=\"triggerCommand('indent')\"\n [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('indent')\">\n </button>\n <button [id]=\"'outdent-'+id\" aeButton iconName=\"outdent\" title=\"Outdent\"\n (click)=\"triggerCommand('outdent')\"\n [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('outdent')\">\n </button>\n </div>\n <div aeToolbarSet>\n <button [id]=\"'insertUnorderedList-'+id\" aeButton iconName=\"unorderedList\" title=\"Unordered List\"\n (click)=\"triggerCommand('insertUnorderedList')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('insertUnorderedList')\">\n </button>\n <button [id]=\"'insertOrderedList-'+id\" aeButton iconName=\"orderedList\" title=\"Ordered List\"\n (click)=\"triggerCommand('insertOrderedList')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('insertOrderedList')\">\n </button>\n </div>\n <div aeToolbarSet>\n <ae-select class=\"select-heading\" [options]=\"headings\"\n [(ngModel)]=\"block\"\n (change)=\"triggerCommand(block)\"\n [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('heading')\"\n tabindex=\"-1\"></ae-select>\n </div>\n <div aeToolbarSet>\n <ae-select class=\"select-font\" [options]=\"fonts\"\n [(ngModel)]=\"fontName\"\n (change)=\"setFontName(fontName)\"\n [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('fontName')\"\n tabindex=\"-1\"></ae-select>\n </div>\n <div aeToolbarSet>\n <ae-select class=\"select-font-size\" [options]=\"fontSizes\"\n [(ngModel)]=\"fontSize\"\n (change)=\"setFontSize(fontSize)\"\n [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('fontSize')\"\n tabindex=\"-1\">\n </ae-select>\n </div>\n <div aeToolbarSet>\n <input\n style=\"display: none\"\n type=\"color\" (change)=\"insertColor(fgInput.value, 'textColor')\"\n #fgInput>\n <button [id]=\"'foregroundColorPicker-'+id\" aeButton iconName=\"textColor\"\n (click)=\"focus(); ; fgInput.click()\"\n title=\"Text Color\"\n [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('textColor')\">\n </button>\n <input\n style=\"display: none\"\n type=\"color\" (change)=\"insertColor(bgInput.value, 'backgroundColor')\"\n #bgInput>\n <button [id]=\"'backgroundColorPicker-'+id\" aeButton iconName=\"backgroundColor\"\n (click)=\"focus(); ; bgInput.click()\"\n title=\"Background Color\"\n [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('backgroundColor')\">\n </button>\n </div>\n <div *ngIf=\"_customClasses\" aeToolbarSet>\n <ae-select class=\"select-custom-style\" [options]=\"customClassList\"\n [(ngModel)]=\"customClassId\"\n (change)=\"setCustomClass(customClassId)\"\n [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('customClasses')\"\n tabindex=\"-1\"></ae-select>\n </div>\n <div aeToolbarSet>\n <button [id]=\"'link-'+id\" aeButton iconName=\"link\" (click)=\"insertUrl()\"\n title=\"Insert Link\" [disabled]=\"isLinkButtonDisabled\" [hidden]=\"isButtonHidden('link')\">\n </button>\n <button [id]=\"'unlink-'+id\" aeButton iconName=\"unlink\" (click)=\"triggerCommand('unlink')\"\n title=\"Unlink\" [disabled]=\"htmlMode || !linkSelected\" [hidden]=\"isButtonHidden('unlink')\">\n </button>\n <input\n style=\"display: none\"\n accept=\"image/*\"\n type=\"file\" (change)=\"onFileChanged($event)\"\n #fileInput>\n <button [id]=\"'insertImage-'+id\" aeButton iconName=\"image\" (click)=\"focus(); fileInput.click()\"\n title=\"Insert Image\"\n [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('insertImage')\">\n </button>\n <button [id]=\"'insertVideo-'+id\" aeButton iconName=\"video\"\n (click)=\"insertVideo()\" title=\"Insert Video\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('insertVideo')\">\n </button>\n <button [id]=\"'insertHorizontalRule-'+id\" aeButton iconName=\"horizontalLine\" title=\"Horizontal Line\"\n (click)=\"triggerCommand('insertHorizontalRule')\" [disabled]=\"htmlMode\"\n [hidden]=\"isButtonHidden('insertHorizontalRule')\">\n </button>\n </div>\n <div aeToolbarSet>\n <button [id]=\"'clearFormatting-'+id\" aeButton iconName=\"removeFormat\" title=\"Clear Formatting\"\n class=\"angular-editor-button\"\n (click)=\"triggerCommand('removeFormat')\" [disabled]=\"htmlMode\" [hidden]=\"isButtonHidden('removeFormat')\">\n </button>\n </div>\n <div aeToolbarSet>\n <button [id]=\"'toggleEditorMode-'+id\" aeButton iconName=\"htmlCode\" title=\"HTML Code\"\n (click)=\"triggerCommand('toggleEditorMode')\" [hidden]=\"isButtonHidden('toggleEditorMode')\">\n </button>\n </div>\n <ng-content></ng-content>\n</div>\n", styles: ["a{cursor:pointer}.angular-editor-toolbar{font:100 14px/15px Roboto,Arial,sans-serif;background-color:var(--ae-toolbar-bg-color, #f5f5f5);font-size:.8rem;padding:var(--ae-toolbar-padding, .2rem);border:var(--ae-toolbar-border-radius, 1px solid #ddd);display:flex;flex-wrap:wrap;gap:4px}.select-heading{display:inline-block;width:90px}@supports not (-moz-appearance: none){.select-heading optgroup{font-size:12px;background-color:#f4f4f4;padding:5px}.select-heading option{border:1px solid;background-color:#fff}}.select-heading:disabled{background-color:#f5f5f5;pointer-events:none;cursor:not-allowed}.select-heading:hover{cursor:pointer;background-color:#f1f1f1;transition:.2s ease}.select-font{display:inline-block;width:90px}@supports not (-moz-appearance: none){.select-font optgroup{font-size:12px;background-color:#f4f4f4;padding:5px}.select-font option{border:1px solid;background-color:#fff}}.select-font:disabled{background-color:#f5f5f5;pointer-events:none;cursor:not-allowed}.select-font:hover{cursor:pointer;background-color:#f1f1f1;transition:.2s ease}.select-font-size{display:inline-block;width:50px}@supports not (-moz-appearance: none){.select-font-size optgroup{font-size: