silex-website-builder-goodevent
Version:
Free and easy website builder for everyone.
303 lines (278 loc) • 10.8 kB
text/typescript
/**
* 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;
}
}