import {Inject, Injectable} from '@angular/core'; import {HttpClient, HttpEvent} from '@angular/common/http'; import {Observable} from 'rxjs'; import {DOCUMENT} from '@angular/common'; import {CustomClass} from './config'; export interface UploadResponse { imageUrl: string; } @Injectable({ providedIn: 'root' }) export class AngularEditorService { savedSelection: Range | null; selectedText: string; uploadUrl: string; constructor( private http: HttpClient, @Inject(DOCUMENT) private doc: any ) { } /** * Executed command from editor header buttons exclude toggleEditorMode * @param command string from triggerCommand */ executeCommand(command: string) { 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, null); } /** * Create URL link * @param url string from UI prompt */ createLink(url: string) { if (!url.includes('http')) { this.doc.execCommand('createlink', false, url); } else { const newUrl = '' + this.selectedText + ''; 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: string, where: string): void { 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: string) { this.doc.execCommand('fontName', false, fontName); } /** * Set font size * @param fontSize string */ setFontSize(fontSize: string) { this.doc.execCommand('fontSize', false, fontSize); } /** * Create raw HTML * @param html HTML string */ insertHtml(html: string): void { 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 */ public saveSelection = (): void => { 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(): boolean { 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 */ public executeInNextQueueIteration(callbackFn: (...args: any[]) => any, timeout = 1e2): void { setTimeout(callbackFn, timeout); } /** check any selection is made or not */ private checkSelection(): any { 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: File): Observable> { const uploadData: FormData = new FormData(); uploadData.append('file', file, file.name); return this.http.post(this.uploadUrl, uploadData, { reportProgress: true, observe: 'events', }); } /** * Insert image with Url * @param imageUrl The imageUrl. */ insertImage(imageUrl: string) { this.doc.execCommand('insertImage', false, imageUrl); } setDefaultParagraphSeparator(separator: string) { this.doc.execCommand('defaultParagraphSeparator', false, separator); } createCustomClass(customClass: CustomClass) { let newTag = this.selectedText; if (customClass) { const tagName = customClass.tag ? customClass.tag : 'span'; newTag = '<' + tagName + ' class="' + customClass.class + '">' + this.selectedText + ''; } this.insertHtml(newTag); } insertVideo(videoUrl: string) { if (videoUrl.match('www.youtube.com')) { this.insertYouTubeVideoTag(videoUrl); } if (videoUrl.match('vimeo.com')) { this.insertVimeoVideoTag(videoUrl); } } private insertYouTubeVideoTag(videoUrl: string): void { const id = videoUrl.split('v=')[1]; const imageUrl = `https://img.youtube.com/vi/${id}/0.jpg`; const thumbnail = `
click to watch
`; this.insertHtml(thumbnail); } private insertVimeoVideoTag(videoUrl: string): void { 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 = `
${data.title}
`; 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); } }); } }