UNPKG

framework7

Version:

Full featured mobile HTML framework for building iOS & Android apps

516 lines (509 loc) 19.1 kB
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;