@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
JavaScript
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: