framework7
Version:
Full featured mobile HTML framework for building iOS & Android apps
516 lines (509 loc) • 19.1 kB
JavaScript
import { getWindow, getDocument } from 'ssr-window';
import $ from '../../shared/dom7.js';
import { extend, deleteProps } from '../../shared/utils.js';
import Framework7Class from '../../shared/class.js';
import { getDevice } from '../../shared/get-device.js';
const textEditorButtonsMap = {
// f7-icon, material-icon, command
bold: ['bold', 'format_bold', 'bold'],
italic: ['italic', 'format_italic', 'italic'],
underline: ['underline', 'format_underlined', 'underline'],
strikeThrough: ['strikethrough', 'strikethrough_s', 'strikeThrough'],
orderedList: ['list_number', 'format_list_numbered', 'insertOrderedList'],
unorderedList: ['list_bullet', 'format_list_bulleted', 'insertUnorderedList'],
link: ['link', 'link', 'createLink'],
image: ['photo', 'image', 'insertImage'],
paragraph: ['paragraph', '<i class="icon">¶</i>', 'formatBlock.P'],
h1: ['<i class="icon">H<sub>1</sub></i>', '<i class="icon">H<sub>1</sub></i>', 'formatBlock.H1'],
h2: ['<i class="icon">H<sub>2</sub></i>', '<i class="icon">H<sub>2</sub></i>', 'formatBlock.H2'],
h3: ['<i class="icon">H<sub>3</sub></i>', '<i class="icon">H<sub>3</sub></i>', 'formatBlock.H3'],
alignLeft: ['text_alignleft', 'format_align_left', 'justifyLeft'],
alignCenter: ['text_aligncenter', 'format_align_center', 'justifyCenter'],
alignRight: ['text_alignright', 'format_align_right', 'justifyRight'],
alignJustify: ['text_justify', 'format_align_justify', 'justifyFull'],
subscript: ['textformat_subscript', '<i class="icon">A<sub>1</sub></i>', 'subscript'],
superscript: ['textformat_superscript', '<i class="icon">A<sup>1</sup></i>', 'superscript'],
indent: ['increase_indent', 'format_indent_increase', 'indent'],
outdent: ['decrease_indent', 'format_indent_decrease', 'outdent']
};
class TextEditor extends Framework7Class {
constructor(app, params) {
super(params, [app]);
const self = this;
const document = getDocument();
const device = getDevice();
const defaults = extend({}, app.params.textEditor);
// Extend defaults with modules params
self.useModulesParams(defaults);
self.params = extend(defaults, params);
const el = self.params.el;
if (!el) return self;
const $el = $(el);
if ($el.length === 0) return self;
if ($el[0].f7TextEditor) return $el[0].f7TextEditor;
let $contentEl = $el.children('.text-editor-content');
if (!$contentEl.length) {
$el.append('<div class="text-editor-content" contenteditable></div>');
$contentEl = $el.children('.text-editor-content');
}
extend(self, {
app,
$el,
el: $el[0],
$contentEl,
contentEl: $contentEl[0]
});
if ('value' in params) {
self.value = self.params.value;
}
if (self.params.mode === 'keyboard-toolbar') {
if (!(device.cordova || device.capacitor) && !device.android) {
self.params.mode = 'popover';
}
}
if (typeof self.params.buttons === 'string') {
try {
self.params.buttons = JSON.parse(self.params.buttons);
} catch (err) {
throw new Error('Framework7: TextEditor: wrong "buttons" parameter format');
}
}
$el[0].f7TextEditor = self;
// Bind
self.onButtonClick = self.onButtonClick.bind(self);
self.onFocus = self.onFocus.bind(self);
self.onBlur = self.onBlur.bind(self);
self.onInput = self.onInput.bind(self);
self.onPaste = self.onPaste.bind(self);
self.onSelectionChange = self.onSelectionChange.bind(self);
self.closeKeyboardToolbar = self.closeKeyboardToolbar.bind(self);
// Handle Events
self.attachEvents = function attachEvents() {
if (self.params.mode === 'toolbar') {
self.$el.find('.text-editor-toolbar').on('click', 'button', self.onButtonClick);
}
if (self.params.mode === 'keyboard-toolbar') {
self.$keyboardToolbarEl.on('click', 'button', self.onButtonClick);
self.$el.parents('.page').on('page:beforeout', self.closeKeyboardToolbar);
}
if (self.params.mode === 'popover' && self.popover) {
self.popover.$el.on('click', 'button', self.onButtonClick);
}
self.$contentEl.on('paste', self.onPaste);
self.$contentEl.on('focus', self.onFocus);
self.$contentEl.on('blur', self.onBlur);
self.$contentEl.on('input', self.onInput, true);
$(document).on('selectionchange', self.onSelectionChange);
};
self.detachEvents = function detachEvents() {
if (self.params.mode === 'toolbar') {
self.$el.find('.text-editor-toolbar').off('click', 'button', self.onButtonClick);
}
if (self.params.mode === 'keyboard-toolbar') {
self.$keyboardToolbarEl.off('click', 'button', self.onButtonClick);
self.$el.parents('.page').off('page:beforeout', self.closeKeyboardToolbar);
}
if (self.params.mode === 'popover' && self.popover) {
self.popover.$el.off('click', 'button', self.onButtonClick);
}
self.$contentEl.off('paste', self.onPaste);
self.$contentEl.off('focus', self.onFocus);
self.$contentEl.off('blur', self.onBlur);
self.$contentEl.off('input', self.onInput, true);
$(document).off('selectionchange', self.onSelectionChange);
};
// Install Modules
self.useModules();
// Init
self.init();
return self;
}
setValue(newValue) {
const self = this;
const currentValue = self.value;
if (currentValue === newValue) return self;
self.value = newValue;
self.$contentEl.html(newValue);
self.$el.trigger('texteditor:change', self.value);
self.emit('local::change textEditorChange', self, self.value);
return self;
}
getValue() {
const self = this;
return self.value;
}
clearValue() {
const self = this;
self.setValue('');
if (self.params.placeholder && !self.$contentEl.html()) {
self.insertPlaceholder();
}
return self;
}
createLink() {
const self = this;
const window = getWindow();
const document = getDocument();
const currentSelection = window.getSelection();
const selectedNodes = [];
let $selectedLinks;
if (currentSelection && currentSelection.anchorNode && $(currentSelection.anchorNode).parents(self.$el).length) {
let anchorNode = currentSelection.anchorNode;
while (anchorNode) {
selectedNodes.push(anchorNode);
if (!anchorNode.nextSibling || anchorNode === currentSelection.focusNode) {
anchorNode = null;
}
if (anchorNode) {
anchorNode = anchorNode.nextSibling;
}
}
const selectedNodesLinks = [];
const $selectedNodes = $(selectedNodes);
for (let i = 0; i < $selectedNodes.length; i += 1) {
const childNodes = $selectedNodes[i].children;
if (childNodes) {
for (let j = 0; j < childNodes.length; j += 1) {
if ($(childNodes[j]).is('a')) {
selectedNodesLinks.push(childNodes[j]);
}
}
}
}
$selectedLinks = $selectedNodes.closest('a').add($(selectedNodesLinks));
}
if ($selectedLinks && $selectedLinks.length) {
$selectedLinks.each(linkNode => {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(linkNode);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('unlink', false);
selection.removeAllRanges();
});
return self;
}
const currentRange = self.getSelectionRange();
if (!currentRange) return self;
const dialog = self.app.dialog.prompt('', self.params.linkUrlText, link => {
if (link && link.trim().length) {
self.setSelectionRange(currentRange);
document.execCommand('createLink', false, link.trim());
self.$el.trigger('texteditor:insertlink', {
url: link.trim()
});
self.emit('local:insertLink textEditorInsertLink', self, link.trim());
}
});
dialog.$el.find('input').focus();
return self;
}
insertImage() {
const self = this;
const document = getDocument();
const currentRange = self.getSelectionRange();
if (!currentRange) return self;
const dialog = self.app.dialog.prompt('', self.params.imageUrlText, imageUrl => {
if (imageUrl && imageUrl.trim().length) {
self.setSelectionRange(currentRange);
document.execCommand('insertImage', false, imageUrl.trim());
self.$el.trigger('texteditor:insertimage', {
url: imageUrl.trim()
});
self.emit('local:insertImage textEditorInsertImage', self, imageUrl.trim());
}
});
dialog.$el.find('input').focus();
return self;
}
removePlaceholder() {
const self = this;
self.$contentEl.find('.text-editor-placeholder').remove();
}
insertPlaceholder() {
const self = this;
self.$contentEl.append(`<div class="text-editor-placeholder">${self.params.placeholder}</div>`);
}
onSelectionChange() {
const self = this;
const window = getWindow();
const document = getDocument();
if (self.params.mode === 'toolbar') return;
const selection = window.getSelection();
const selectionIsInContent = $(selection.anchorNode).parents(self.contentEl).length || selection.anchorNode === self.contentEl;
if (self.params.mode === 'keyboard-toolbar') {
if (!selectionIsInContent) {
self.closeKeyboardToolbar();
} else {
self.openKeyboardToolbar();
}
return;
}
if (self.params.mode === 'popover') {
const selectionIsInPopover = $(selection.anchorNode).parents(self.popover.el).length || selection.anchorNode === self.popover.el;
if (!selectionIsInContent && !selectionIsInPopover) {
self.closePopover();
return;
}
if (!selection.isCollapsed && selection.rangeCount) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const rootEl = self.app.$el[0] || document.body;
self.openPopover(rect.x + (window.scrollX || 0) - rootEl.offsetLeft, rect.y + (window.scrollY || 0) - rootEl.offsetTop, rect.width, rect.height);
} else if (selection.isCollapsed) {
self.closePopover();
}
}
}
onPaste(e) {
const self = this;
const document = getDocument();
if (self.params.clearFormattingOnPaste && e.clipboardData && e.clipboardData.getData) {
const text = e.clipboardData.getData('text/plain');
e.preventDefault();
document.execCommand('insertText', false, text);
}
}
onInput() {
const self = this;
const value = self.$contentEl.html();
self.value = value;
self.$el.trigger('texteditor:input');
self.emit('local:input textEditorInput', self, self.value);
self.$el.trigger('texteditor:change', self.value);
self.emit('local::change textEditorChange', self, self.value);
}
onFocus() {
const self = this;
self.removePlaceholder();
self.$contentEl.focus();
self.$el.trigger('texteditor:focus');
self.emit('local::focus textEditorFocus', self);
}
onBlur() {
const self = this;
const window = getWindow();
const document = getDocument();
if (self.params.placeholder && self.$contentEl.html() === '') {
self.insertPlaceholder();
}
if (self.params.mode === 'popover') {
const selection = window.getSelection();
const selectionIsInContent = $(selection.anchorNode).parents(self.contentEl).length || selection.anchorNode === self.contentEl;
const inPopover = document.activeElement && self.popover && $(document.activeElement).closest(self.popover.$el).length;
if (!inPopover && !selectionIsInContent) {
self.closePopover();
}
}
if (self.params.mode === 'keyboard-toolbar') {
const selection = window.getSelection();
const selectionIsInContent = $(selection.anchorNode).parents(self.contentEl).length || selection.anchorNode === self.contentEl;
if (!selectionIsInContent) {
self.closeKeyboardToolbar();
}
}
self.$el.trigger('texteditor:blur');
self.emit('local::blur textEditorBlur', self);
}
onButtonClick(e) {
const self = this;
const window = getWindow();
const document = getDocument();
const selection = window.getSelection();
const selectionIsInContent = $(selection.anchorNode).parents(self.contentEl).length || selection.anchorNode === self.contentEl;
if (!selectionIsInContent) return;
const $buttonEl = $(e.target).closest('button');
if ($buttonEl.parents('form').length) {
e.preventDefault();
}
const button = $buttonEl.attr('data-button');
const buttonData = self.params.customButtons && self.params.customButtons[button];
if (!button || !(textEditorButtonsMap[button] || buttonData)) return;
$buttonEl.trigger('texteditor:buttonclick', button);
self.emit('local::buttonClick textEditorButtonClick', self, button);
if (buttonData) {
if (buttonData.onClick) buttonData.onClick(self, $buttonEl[0]);
return;
}
const command = textEditorButtonsMap[button][2];
if (command === 'createLink') {
self.createLink();
return;
}
if (command === 'insertImage') {
self.insertImage();
return;
}
if (command.indexOf('formatBlock') === 0) {
const tagName = command.split('.')[1];
const $anchorNode = $(selection.anchorNode);
if ($anchorNode.parents(tagName.toLowerCase()).length || $anchorNode.is(tagName)) {
document.execCommand('formatBlock', false, 'div');
} else {
document.execCommand('formatBlock', false, tagName);
}
return;
}
document.execCommand(command, false);
}
// eslint-disable-next-line
getSelectionRange() {
const window = getWindow();
const document = getDocument();
if (window.getSelection) {
const sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
return sel.getRangeAt(0);
}
} else if (document.selection && document.selection.createRange) {
return document.selection.createRange();
}
return null;
}
// eslint-disable-next-line
setSelectionRange(range) {
const window = getWindow();
const document = getDocument();
if (range) {
if (window.getSelection) {
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (document.selection && range.select) {
range.select();
}
}
}
renderButtons() {
const self = this;
let html = '';
function renderButton(button) {
const iconClass = self.app.theme === 'md' ? 'material-icons' : 'f7-icons';
if (self.params.customButtons && self.params.customButtons[button]) {
const buttonData = self.params.customButtons[button];
return `<button type="button" class="text-editor-button" data-button="${button}">${buttonData.content || ''}</button>`;
}
if (!textEditorButtonsMap[button]) return '';
const iconContent = textEditorButtonsMap[button][self.app.theme === 'md' ? 1 : 0];
return `<button type="button" class="text-editor-button" data-button="${button}">${iconContent.indexOf('<') >= 0 ? iconContent : `<i class="${iconClass}">${iconContent}</i>`}</button>`.trim();
}
self.params.buttons.forEach((button, buttonIndex) => {
if (Array.isArray(button)) {
button.forEach(b => {
html += renderButton(b);
});
if (buttonIndex < self.params.buttons.length - 1 && self.params.dividers) {
html += '<div class="text-editor-button-divider"></div>';
}
} else {
html += renderButton(button);
}
});
return html;
}
createToolbar() {
const self = this;
self.$el.prepend(`<div class="text-editor-toolbar">${self.renderButtons()}</div>`);
}
createKeyboardToolbar() {
const self = this;
self.$keyboardToolbarEl = $(`<div class="toolbar toolbar-bottom text-editor-keyboard-toolbar"><div class="toolbar-inner">${self.renderButtons()}</div></div>`);
}
createPopover() {
const self = this;
self.popover = self.app.popover.create({
content: `
<div class="popover dark text-editor-popover">
<div class="popover-inner">${self.renderButtons()}</div>
</div>
`,
closeByOutsideClick: false,
backdrop: false
});
}
openKeyboardToolbar() {
const self = this;
if (self.$keyboardToolbarEl.parent(self.app.$el).length) return;
self.$el.trigger('texteditor:keyboardopen');
self.emit('local::keyboardOpen textEditorKeyboardOpen', self);
self.app.$el.append(self.$keyboardToolbarEl);
}
closeKeyboardToolbar() {
const self = this;
self.$keyboardToolbarEl.remove();
self.$el.trigger('texteditor:keyboardclose');
self.emit('local::keyboardClose textEditorKeyboardClose', self);
}
openPopover(targetX, targetY, targetWidth, targetHeight) {
const self = this;
if (!self.popover) return;
Object.assign(self.popover.params, {
targetX,
targetY,
targetWidth,
targetHeight
});
clearTimeout(self.popoverTimeout);
self.popoverTimeout = setTimeout(() => {
if (!self.popover) return;
if (self.popover.opened) {
self.popover.resize();
} else {
self.$el.trigger('texteditor:popoveropen');
self.emit('local::popoverOpen textEditorPopoverOpen', self);
self.popover.open();
}
}, 400);
}
closePopover() {
const self = this;
clearTimeout(self.popoverTimeout);
if (!self.popover || !self.popover.opened) return;
self.popoverTimeout = setTimeout(() => {
if (!self.popover) return;
self.$el.trigger('texteditor:popoverclose');
self.emit('local::popoverClose textEditorPopoverClose', self);
self.popover.close();
}, 400);
}
init() {
const self = this;
if (self.value) {
self.$contentEl.html(self.value);
} else {
self.value = self.$contentEl.html();
}
if (self.params.placeholder && self.value === '') {
self.insertPlaceholder();
}
if (self.params.mode === 'toolbar') {
self.createToolbar();
} else if (self.params.mode === 'popover') {
self.createPopover();
} else if (self.params.mode === 'keyboard-toolbar') {
self.createKeyboardToolbar();
}
self.attachEvents();
self.$el.trigger('texteditor:init');
self.emit('local::init textEditorInit', self);
return self;
}
destroy() {
let self = this;
self.$el.trigger('texteditor:beforedestroy');
self.emit('local::beforeDestroy textEditorBeforeDestroy', self);
self.detachEvents();
if (self.params.mode === 'keyboard-toolbar' && self.$keyboardToolbarEl) {
self.$keyboardToolbarEl.remove();
}
if (self.popover) {
self.popover.close(false);
self.popover.destroy();
}
delete self.$el[0].f7TextEditor;
deleteProps(self);
self = null;
}
}
export default TextEditor;