/** * Silex, live web creation * http://projects.silexlabs.org/?/silex/ * * Copyright (c) 2012 Silex Labs * http://www.silexlabs.org/ * * Silex is available under the GPL license * http://www.silexlabs.org/silex/silex-licensing/ */ /** * @fileoverview * This class handle a div positioned on top of the selected text in a text * box It uses the wysihtml library to change text format */ import { wysihtml, WysiHtmlEditor } from '../externs'; import { Body } from '../model/body'; import { Tracker } from '../service/tracker'; import { Controller, LinkData, Model } from '../types'; import { SilexNotification } from '../utils/notification'; import { FileExplorer } from '../view/dialog/file-explorer'; import { LINK_ATTRIBUTES, LinkDialog } from './dialog/LinkDialog'; /** * @class {silex.view.TextFormatBar} */ export class TextFormatBar { // tracker for analytics tracker: any; // store the params selectedElements: HTMLElement[] = null; pageNames: string[] = null; currentPageName: string = null; currentTextBox: HTMLElement = null; wysihtmlEditor: WysiHtmlEditor = null; linkDialog: any; toolbar: any; // for event add / remove onKeyDownBinded: any; onScrollBinded: any; /** * * @param element container to render the UI * @param model model class which holds * the model instances - views use it for * read operation only * @param controller structure which holds * the controller instances */ constructor(protected element: HTMLElement, protected model: Model, protected controller: Controller) { this.tracker = Tracker.getInstance(); this.linkDialog = new LinkDialog(this.model); this.toolbar = this.element.querySelector('#wysihtml5-toolbar'); this.onKeyDownBinded = this.onKeyDown.bind(this); this.onScrollBinded = this.onScroll.bind(this); } /** * get the link of selected text in text editor * this uses a hidden text field in the text format bar, which has a value set * by wysihtml */ getLink(): LinkData { return LINK_ATTRIBUTES.reduce((acc, attr) => { const el = this.element.querySelector('.get-' + attr) as HTMLInputElement; if (!el) { console.error( 'could not get data from link editor for attribute', attr); } else { acc[attr] = el.value; } return acc; }, {}); } /** * Intercept keys before it is forwarded from iframe to Silex */ onKeyDown(e) { if (e.key === 'Escape') { // stop editing but keep selection this.stopEditing(); e.preventDefault(); e.stopPropagation(); } } onScroll(e) { this.controller.textEditorController.attachToTextBox(this.currentTextBox, this.element); } /** * stop edit, destroy wysihtml object and reset everything */ stopEditing() { this.controller.stageController.stopEdit(); if (this.wysihtmlEditor) { // remove event listener const doc = this.model.file.getContentDocument(); const win = this.model.file.getContentWindow(); doc.removeEventListener('keydown', this.onKeyDownBinded); win.removeEventListener('scroll', this.onScrollBinded); // remove and put back the whole UI // this is the way to go with wysihtml // @see https://github.com/Voog/wysihtml/issues/109#issuecomment-198350743 (this.element.querySelector('.image-details') as HTMLElement).style.display = 'none'; this.wysihtmlEditor.destroy(); this.wysihtmlEditor = null; const parent = this.toolbar.parentElement; const clone = this.toolbar.cloneNode(true); Array.from(clone.querySelectorAll('.wysihtml-command-active')) .forEach((el: HTMLElement) => el.classList.remove('wysihtml-command-active')); parent.insertBefore(clone, this.toolbar); parent.removeChild(this.toolbar); this.toolbar = clone; // reset focus Body.resetFocus(); this.controller.stageController.refreshView(); // use array acces for getSelection as a workaround for google closure // warning 'Property getSelection never defined on Document' cleanup const editable = this.model.element.getContentNode(this.currentTextBox); editable.removeAttribute('contenteditable'); editable.classList.remove('wysihtml-sandbox', 'wysihtml-editor'); this.currentTextBox.classList.remove('text-editor-focus'); this.currentTextBox.removeAttribute('data-allow-silex-shortcuts'); this.currentTextBox.onclick = null; this.currentTextBox = null; } this.element.classList.remove('text-editor-editing'); } startEditing(fileExplorer: FileExplorer) { // edit the style of the selection if (this.selectedElements.length === 1) { const newTextBox = this.selectedElements[0]; if (newTextBox !== this.currentTextBox) { this.stopEditing(); this.controller.stageController.startEdit(); this.currentTextBox = newTextBox; // this.currentTextBox.insertBefore(this.element, // this.currentTextBox.firstChild); this.controller.textEditorController.attachToTextBox(this.currentTextBox, this.element); const editable = this.model.element.getContentNode(this.currentTextBox); const options = { toolbar: this.toolbar, handleTables: false, useLineBreaks: false, classes: { 'wysiwyg-float-left': 1, // this doesnt work for some reason 'wysiwyg-float-right': 1, }, // this doesnt work for some reason parserRules: { tags: { b: {}, strong: {rename_tag: 'b'}, i: {}, br: {}, p: {}, h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, h6: {}, ul: {}, u: {}, ol: {}, li: {}, a: { check_attributes: { href: 'href', download: 'href', target: 'any', title: 'any', type: 'any', }, }, img: { check_attributes: { src: 'src', alt: 'alt', title: 'any', width: 'any', height: 'any', class: 'any', }, }, // this should not be necessary, workaround font: {rename_tag: 'span', add_class: {size: 'size_font'}}, }, }, }; this.wysihtmlEditor = new wysihtml.Editor(editable, options); // CSS classes this.currentTextBox.classList.add('text-editor-focus'); this.currentTextBox.setAttribute('data-allow-silex-shortcuts', 'true'); this.element.classList.add('text-editor-editing'); // handle the focus const doc = this.model.file.getContentDocument(); const win = this.model.file.getContentWindow(); doc.addEventListener('keydown', this.onKeyDownBinded); win.addEventListener('scroll', this.onScrollBinded); this.wysihtmlEditor.on('blur', (e) => { if (!SilexNotification.isActive) { this.stopEditing(); } }); this.wysihtmlEditor.on('load', () => { this.wysihtmlEditor.focus(false); setTimeout(() => { if (this.wysihtmlEditor) { this.wysihtmlEditor.focus(false); } }, 250); setTimeout(() => { if (this.wysihtmlEditor) { this.wysihtmlEditor.focus(false); } }, 500); (this.element.querySelector('.insert-image') as HTMLElement).onclick = (e) => { this.tracker.trackAction('controller-events', 'request', 'insert.image.text', 0); fileExplorer.openFile(FileExplorer.IMAGE_EXTENSIONS) .then((fileInfo) => { if (fileInfo) { // undo checkpoint this.controller.textEditorController.undoCheckPoint(); this.wysihtmlEditor.composer.commands.exec('insertImage', {src: fileInfo.absPath, alt: ''}); this.tracker.trackAction('controller-events', 'success', 'insert.image.text', 1); } }) .catch((error) => { SilexNotification.notifyError('Error: I did not manage to load the image. \n' + (error.message || '')); this.tracker.trackAction('controller-events', 'error', 'insert.image.text', -1); }); }; // image details UI const imageDetails = this.element.querySelector('.image-details') as HTMLElement; const autoBtn = imageDetails.querySelector('#auto-submit-image') as HTMLElement; function autoSubmitImage(e) { // give time to the value to be updated and validate wysihtml image // dialog setTimeout(() => { autoBtn.click(); imageDetails.style.display = ''; (e.target as HTMLElement).focus(); }, 100); } (imageDetails.querySelector('.float') as HTMLElement).onchange = (e) => autoSubmitImage(e); (imageDetails.querySelector('.src') as HTMLElement).onkeydown = (e) => autoSubmitImage(e); (imageDetails.querySelector('.alt') as HTMLElement).onkeydown = (e) => autoSubmitImage(e); (this.element.querySelector('.create-link') as HTMLElement).onclick = (e) => { // open link editor this.linkDialog.open(this.getLink(), this.pageNames, (_options) => { if (_options) { this.wysihtmlEditor.composer.commands.exec('createLink', _options); } else { this.wysihtmlEditor.composer.commands.exec('removeLink'); } }); // prevent click on the button e.preventDefault(); }; }); } } else { console.error('Error, can not edit selection with format pane', this.selectedElements); } } /** * redraw the properties * @param selectedElements the elements currently selected * @param pageNames the names of the pages which appear in the current HTML file * @param currentPageName the name of the current page */ redraw(selectedElements: HTMLElement[], pageNames: string[], currentPageName: string) { // reuse selectedElements in combo box on change this.selectedElements = selectedElements; // reuse pageNames in combo box on change this.pageNames = pageNames; this.currentPageName = currentPageName; } }