UNPKG

@striven-erp/striven-editor

Version:
1,370 lines (1,087 loc) 142 kB
import './striveneditor.css'; import { EXTENSIONS, FONTPACK, OPTIONGROUPS, DEFAULTOPTIONS, ACTIVEOPTIONCOLOR, FORECOLORICON, HILITECOLORICON, FONTNAMES, FONTSIZES, COLLAPSEICON, EXPANDICON, DESIGNICON, UPLOADICON } from './defaults.js'; // Helpers import { createSVG, denormalizeCamel, blowUpElement, getImageDimensions, getImageDataURL, computeImageDimensions, createImageElement } from './utils'; // Polyfills import ResizeObserver from 'resize-observer-polyfill'; // Plugins import linkify from 'linkifyjs/element'; // Pickr //import './classic.min.css'; //import Pickr from './pickr.min.js'; import '@simonwep/pickr/dist/themes/classic.min.css'; // 'classic' theme import Pickr from '@simonwep/pickr/dist/pickr.es5.min'; // Formatting import js_beautify from 'js-beautify'; /* Represents an instance of the Striven Editor */ export default class StrivenEditor { /** * Bind onchange handler to contenteditable element * @param {HTMLElement} element to bind onchange to */ _bindContenteditableOnChange(el) { const se = this; el.addEventListener('focus', function () { if (el.data_orig === undefined) { el.data_orig = el.innerHTML; } }); el.addEventListener('blur', function () { const menus = se.editor.getElementsByClassName('se-popup-open:not(.se-toolbar-group)'); const inputs = se.editor.getElementsByTagName('input'); const colorPicker = se.editor.querySelectorAll('.pcr-app.visible'); const actives = [...menus, ...colorPicker, ...inputs, se.body, se.toolbar, se.editor]; if ( el.innerHTML != el.data_orig && !se.toolbarClick && !actives.includes(document.activeElement) && !menus.length && !colorPicker.length ) { se.createLinks(); if (se.options.change) { se.options.change(se.getContent()); } delete el.data_orig; } }); } /** * Instantiate the StrivenEditor * @param {HTMLElement} el The element to initialize StrivenEditor on * @param {Object} options StrivenEditor options to initialize the editor with */ constructor(el, options) { const se = this; // Webpack inserts the package.json version se['_version'] = __VERSION__; // Establish the browser context se.establishBrowser(); // Initialize a range instance for the editor se.range = new Range(); // Initialize a file array for file upload se.files = []; // Default Option Groups with SVG Data se.optionGroups = OPTIONGROUPS; // Gets whether an image is being uploaded when pasting or inserting an image se._isImageUploading = false; // Initialize Options if (options) { // Set options property se.options = options; // Font Pack for font-awesome (not being used) options.fontPack || (se.options.fontPack = FONTPACK); // Allowed File Extensions options.extensions || (se.options.extensions = EXTENSIONS); // Enabled Toolbar options options.toolbarOptions || (se.options.toolbarOptions = DEFAULTOPTIONS); // Fill color for active options options.activeOptionColor || (se.options.activeOptionColor = ACTIVEOPTIONCOLOR); // Default fonts names options.fontNames || (se.options.fontNames = FONTNAMES); // Allow File Upload options.fileUpload !== false && (se.options.fileUpload = true); // Configure Options with Minimal Enabled if (options.toolbarOptions && options.minimal) { // Get custom options from toolbarOptions const customs = options.toolbarOptions.filter((opt) => typeof opt === 'object'); // Set toolbar options for minimal configuration se.options.toolbarOptions = ['bold', 'italic', 'underline', 'insertUnorderedList', 'attachment', 'link', 'image', ...customs]; //disallow tabbing se.options.canTab = false; } } else { // Set default options se.options = { fontPack: FONTPACK, extensions: EXTENSIONS, toolbarOptions: DEFAULTOPTIONS, activeOptionColor: ACTIVEOPTIONCOLOR, fontNames: FONTNAMES, fileUpload: true, canTab: false }; } // Initialize the editor se.initEditor(el); // Core Editor Initialization se.initResponsive(); // Editor Reponsive Logic se.initOverflow(); // Overflow Content Logic se.overflow(); // Trigger overflow login on init se.getIsImageUploading = () => se._isImageUploading; // DOM Access to the Editor Instance el.StrivenEditor = () => se; // Bind handler functions to scope se.bound_popupEscapeHandler = se.popupEscapeHandler.bind(se); } /** * Initiliaze the StrivenEditor on the passed element. * @param {HTMLElement} el */ initEditor(el) { const se = this; se.editor = el; se.toolbar = se.initToolbar(); se.body = se.initBody(); se.linkMenu = se.initLinkMenu(); se.imageMenu = se.initImageMenu(); se.tableMenu = se.initTableMenu(); se.metaDataSection = se.options.metaUrl ? se.initMetaDataSection() : null; se.filesSection = se.options.fileUpload ? se.initFilesSection() : null; // Add Striven Editor Class se.editor.classList.add('striven-editor'); // Stop events from bubbling up the DOM se.editor.onkeypress = (e) => e.stopPropagation(); // Initialze with the value property in the options se.setContent(se.options.value || ''); window.addEventListener('click', () => { if (!se.editor.contains(document.activeElement)) { se.closeAllMenus(); } }); // Remove all link option popups on escape window.addEventListener('keydown', (e) => { if (e.key === 'Escape') { [...se.body.querySelectorAll('.se-link-options')].forEach((o) => o.remove()); [...se.body.querySelectorAll('.se-image-options')].forEach((o) => o.remove()); } }); // Toolbar Hide if (se.options.toolbarHide) { // Hide the toolbar template if there is one se.toolbarTemplate && (se.toolbarTemplate.style.display = 'none'); // Hide the toolbar options se.toolbarOptionsGroup.style.display = 'none'; // Add the close class se.toolbar.classList.add('se-toolbar-close'); // Bind the focus event to reopen the toolbar const bodyFocus = se.body.onfocus; se.body.onfocus = () => { se.overflow(); se.openToolbar(); bodyFocus && bodyFocus(); }; // Bind the blur event close the toolbar const bodyBlur = se.body.onblur; se.body.onblur = () => { bodyBlur && bodyBlur(); se.overflow(); // Do not close the toolbar is editor is active setTimeout(() => { if ( se.linkMenu.dataset.active !== 'true' && se.imageMenu.dataset.active !== 'true' && se.tableMenu.dataset.active !== 'true' && !se.isEditorInFocus() ) { se.closeToolbar(); } }, 200); }; } else { se.toolbar.style.boxShadow = '#ddd -1px 2px 3px 0px'; se.toolbar.style.height = 'fit-content'; } // Toolbar Options se.toolbarOptions.forEach((optionEl) => { // Execute Toolbar Commands const optionElClick = optionEl.onclick; // Bind the toolbar commands click hander optionEl.onclick = (e) => { if (!se.browser.isSafari()) { se.range = se.getRange(); } // Get the document execute command const command = optionEl.id.split('-').pop(); // Command Logic switch (command) { case 'bold': case 'underline': case 'italic': case 'strikethrough': // If active, deactive the command if (optionEl.classList.contains('se-toolbar-option-active')) { optionEl.classList.remove('se-toolbar-option-active'); // Save the range se.setRange(); // Focus back into the body se.body.focus(); // If the command is active, deactivate the command document.queryCommandState(command) && se.executeCommand(command); } else { // Set the command active (body on focus will execute the command) optionEl.classList.add('se-toolbar-option-active'); // Save the range se.setRange(); // Focus back into the body se.body.focus(); } break; case 'removeFormat': se.executeCommand(command); // Deactivate all toolbar options se.toolbarOptions.forEach((o) => o.classList.remove('se-toolbar-option-active')); break; case 'indent': setTimeout(() => se.setRange(se.range), 0); default: se.body.focus(); se.executeCommand(command); break; } optionElClick && optionElClick(); }; }); // Add dividers function constructDivider() { const divider = document.createElement('div'); divider.classList.add('se-divider-section'); const bar = document.createElement('div'); bar.classList.add('se-divider-bar'); divider.append(bar); return divider; } const areas = [...se.toolbarGroups, ...se.toolbar.querySelectorAll('.se-toolbar-selection')]; areas.forEach((a) => { if (a.querySelector('.se-toolbar-option')) { a.after(constructDivider()); } }); // Construct editor elements se.toolbar && se.editor.appendChild(se.toolbar); se.body && se.editor.appendChild(se.body); se.linkMenu && se.editor.appendChild(se.linkMenu); se.imageMenu && se.editor.appendChild(se.imageMenu); se.tableMenu && se.editor.appendChild(se.tableMenu); se.metaDataSection && se.editor.appendChild(se.metaDataSection); se.filesSection && se.editor.appendChild(se.filesSection); // Reposition Toolbar if (se.options.toolbarBottom) { se.toolbar.classList.add('se-toolbar-bottom'); se.toolbar.classList.add('se-toolbar-top'); se.linkMenu.classList.remove('se-popup-top'); se.linkMenu.classList.add('se-popup-bottom'); se.imageMenu.classList.remove('se-popup-top'); se.imageMenu.classList.add('se-popup-bottom'); se.tableMenu.classList.remove('se-popup-top'); se.tableMenu.classList.add('se-popup-bottom'); se.editor.removeChild(se.toolbar); se.editor.append(se.toolbar); } se.options.init && se.options.init(se); } /** * Open the toolbar for when the toolbarHide option is set to true */ openToolbar() { const se = this; se.toolbar.classList.remove('se-toolbar-close'); setTimeout(() => { se.toolbarOptionsGroup.style.display = 'flex'; se.toolbarTemplate && (se.toolbarTemplate.style.display = 'flex'); }, 200); } /** * Close the toolbar for when the toolbarHide option is set to true */ closeToolbar() { const se = this; se.closeAllMenus(); se.toolbarOptionsGroup.style.display = 'none'; se.toolbarTemplate && (se.toolbarTemplate.style.display = 'none'); se.toolbar.classList.add('se-toolbar-close'); } /** * Initialized the toolbar for StrivenEditor * @returns {HTMLElement} The StrivenEditor toolbar */ initToolbar() { const se = this; const toolbar = document.createElement('div'); se.toolbarOptionsGroup = document.createElement('div'); const groups = Object.keys(se.optionGroups); toolbar.classList.add('se-toolbar'); toolbar.classList.add('toolbar-top'); se.toolbarOptionsGroup.classList.add('se-toolbar-options'); toolbar.onclick = (ev) => { se.body.focus(); }; // Append Font Options !se.options.minimal && se.initToolbarFontOptions(); //iterate groups groups.forEach((group) => { // add menu to toolbarOptions const toolbarMenu = document.createElement('div'); // const toolbarMenuIcon = document.createElement("i"); toolbarMenu.classList.add('se-toolbar-menu'); toolbarMenu.id = `menu-${group}`; toolbarMenu.setAttribute('data-name', group); // toolbarMenuIcon.classList.add(se.options.fontPack); // toolbarMenuIcon.classList.add(se.optionGroups[group].menu); const arrow = { viewBox: '0 0 1792 1792', d: 'M1395 736q0 13-10 23l-466 466q-10 10-23 10t-23-10l-466-466q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l393 393 393-393q10-10 23-10t23 10l50 50q10 10 10 23z' }; const svgSpan = se.constructSVG(se.optionGroups[group].menu); toolbarMenu.appendChild(svgSpan.getElementsByTagName('svg')[0]); if (group !== 'options') { const arrowSpan = se.constructSVG(arrow); arrowSpan.classList.add('se-arrow-span'); toolbarMenu.appendChild(arrowSpan); } se.toolbarOptionsGroup.appendChild(toolbarMenu); // add group to toolbarOptions const toolbarGroup = document.createElement('div'); toolbarGroup.classList.add('se-toolbar-group'); toolbarGroup.id = `group-${group}`; // iterate options within group se.options.toolbarOptions.forEach((option) => { const toolbarOption = se.optionGroups[group].group.filter((group) => group[option])[0]; if (toolbarOption) { const svgData = toolbarOption[option]; const optionSpan = se.constructSVG(svgData); optionSpan.classList.add('se-toolbar-option'); optionSpan.id = `toolbar-${option}`; optionSpan.title = denormalizeCamel(option); optionSpan.setAttribute('data-group-name', group); switch (option) { case 'removeFormat': optionSpan.setAttribute('title', 'Clear Format'); break; case 'hiliteColor': optionSpan.setAttribute('title', 'Background Color'); break; default: optionSpan.setAttribute('title', denormalizeCamel(option)); break; } toolbarGroup.appendChild(optionSpan); } }); se.toolbarOptionsGroup.appendChild(toolbarGroup); }); toolbar.appendChild(se.toolbarOptionsGroup); // Custom toolbar template if (se.options.toolbarTemplate) { const toolbarTemplate = document.createElement('div'); toolbarTemplate.id = 'toolbar-template'; toolbarTemplate.setAttribute('style', 'display: flex'); toolbarTemplate.appendChild(se.options.toolbarTemplate); toolbar.appendChild(toolbarTemplate); se.toolbarTemplate = toolbarTemplate; } se.toolbarOptions = toolbar.querySelectorAll('span'); se.toolbarGroups = [...toolbar.getElementsByClassName('se-toolbar-group')]; se.toolbarMenus = [...toolbar.getElementsByClassName('se-toolbar-menu')]; // Remove menu that has no options enabled se.toolbarGroups.forEach((group) => { if (group && group.children.length < 1) { const groupName = group.id.split('-')[1]; const menu = se.toolbarMenus.filter((menu) => menu && menu.id.split('-')[1] === groupName)[0]; menu.remove(); } }); const miscOptions = toolbar.querySelector('#group-options'); // toolbar group for custom options const customOptions = se.options.toolbarOptions.filter((option) => typeof option === 'object'); if (customOptions.length > 0) { customOptions.forEach((opt) => { const { icon, handler, title } = opt; if (typeof icon === 'object') { const option = se.constructSVG({ viewBox: icon.viewBox, d: icon.d }); option.classList.add('se-toolbar-option'); option.onclick = () => handler(option); option.setAttribute('id', `toolbar-${title}`); option.setAttribute('title', denormalizeCamel(title)); miscOptions.appendChild(option); } else { const option = document.createElement('span'); const image = document.createElement('img'); image.setAttribute('src', opt.icon); image.setAttribute('alt', 'custom option'); option.style.paddingTop = '6px'; option.style.paddingBottom = '8px'; option.classList.add('se-toolbar-option'); option.onclick = () => handler(); option.append(image); miscOptions.appendChild(option); } }); } const removeFormatOption = toolbar.querySelector('#toolbar-removeFormat'); if (removeFormatOption) { removeFormatOption.remove(); miscOptions.append(removeFormatOption); } se.toolbarClick = false; toolbar.addEventListener('mousedown', () => { se.toolbarClick = true; }); toolbar.addEventListener('mouseup', () => { se.toolbarClick = false; }); return toolbar; } /* * Initializes the toolbars font options */ initToolbarFontOptions() { const se = this; function initMenu(name, onOpen) { const menu = document.createElement('div'); menu.setAttribute('id', `${name}-menu`); menu.classList.add('se-popup', se.options.toolbarBottom ? 'se-popup-bottom' : 'se-popup-top'); menu.dataset.active = 'false'; menu.open = () => { se.closeAllMenus(); se.setMenuOffset(se.toolbar.querySelector(`#toolbar-${name}`), menu); menu.classList.add('se-popup-open'); menu.dataset.active = 'true'; se.addPopupEscapeHandler(); onOpen && onOpen(menu); }; menu.close = () => { menu.classList.remove('se-popup-open'); menu.dataset.active = 'false'; se.removePopupEscapeHandler(); }; se.editor.append(menu); return menu; } function initFontNameMenu(select) { const menu = initMenu('fontName'); menu.dataset.init = 'true'; ['(inherited font)', ...se.options.fontNames].forEach((f) => { const fontOption = document.createElement('div'); fontOption.classList.add('se-toolbar-popup-option'); fontOption.textContent = f; fontOption.style.fontFamily = f; const trigger = (e) => { let fontselect = e.target.textContent; select.textContent = fontselect; menu.close(); function execute() { if (fontselect === '(inherited font)') { fontselect = getComputedStyle(se.body).fontFamily; } if (se.browser.isEdge() || se.browser.isFirefox()) { document.execCommand('fontName', false, fontselect); } else { document.execCommand('fontName', true, fontselect); } } execute(); const refocus = se.body.onfocus; se.body.onfocus = () => { se.setRange(se.range); if (!se.getContent()) { const enabler = () => { setTimeout(() => { execute(); se.body.removeEventListener('keydown', enabler); }); }; se.body.addEventListener('keydown', enabler); } setTimeout(() => execute(), 0); if (se.scrollPosition && !se.browser.isEdge()) { se.body.scrollTo(se.scrollPosition.x, se.scrollPosition.y); } se.body.onfocus = refocus; }; se.body.focus(); }; fontOption.onmousedown = trigger; menu.append(fontOption); }); return menu; } function initFontSizeMenu(select) { const menu = initMenu('fontSize'); const sizes = FONTSIZES; ['(inherited size)', ...Object.keys(sizes)].forEach((size) => { let s = sizes[size] || '(inherited size)'; const fontOption = document.createElement('div'); fontOption.classList.add('se-toolbar-popup-option'); fontOption.textContent = s; function execute() { let execSize = size; if (size === '(inherited size)') { execSize = 3; } if (se.browser.isEdge() || se.browser.isFirefox()) { document.execCommand('fontSize', false, execSize); } else { document.execCommand('fontSize', true, execSize); } } const trigger = (e) => { const fontsize = e.target.textContent; select.textContent = fontsize; select.dataset.command = size; menu.close(); execute(); const refocus = se.body.focus; se.body.onfocus = () => { se.setRange(se.range); if (!se.getContent()) { const enabler = () => { setTimeout(() => { execute(); se.body.removeEventListener('keydown', enabler); }, 0); }; se.body.addEventListener('keydown', enabler); } setTimeout(() => execute(), 0); if (se.scrollPosition && !se.browser.isEdge()) { se.body.scrollTo(se.scrollPosition.x, se.scrollPosition.y); } se.body.onfocus = refocus; }; se.body.focus(); }; fontOption.onmousedown = trigger; menu.append(fontOption); }); return menu; } function initFontFormatMenu() { const menu = initMenu('fontFormat'); const formats = [ { command: 'H1', option: '<h1 style="margin: 0; color: #000;">Heading 1</h1>' }, { command: 'H2', option: '<h2 style="margin: 0; color: #000;">Heading 2</h2>' }, { command: 'H3', option: '<h3 style="margin: 0; color: #000;">Heading 3</h4>' }, { command: 'H4', option: '<h4 style="margin: 0; color: #000;">Heading 4</h4>' }, { command: 'H5', option: '<h5 style="margin: 0; color: #000;">Heading 5</h5>' }, { command: 'H6', option: '<h6 style="margin: 0; color: #000;">Heading 6</h6>' }, { command: 'P', option: '<p style="margin: 0; color: #000;">Paragraph</p>' } ]; formats.forEach((s) => { const fontOption = document.createElement('div'); fontOption.classList.add('se-toolbar-popup-option'); fontOption.innerHTML = s.option; fontOption.onclick = (e) => { menu.close(); se.body.focus(); se.setRange(); if (se.browser.isFirefox() || se.browser.isEdge()) { document.execCommand('removeFormat', false); } else { document.execCommand('removeFormat', true); } se.executeCommand('formatBlock', s.command); }; menu.append(fontOption); }); return menu; } // Get enabled toolbar options const enabledOptions = se.options.toolbarOptions; // Enable font name option if (enabledOptions.includes('fontName')) { // Initialize and append toolbar option const fontSelect = document.createElement('div'); const selectedFont = document.createElement('p'); // Initialize the popup menu to be length of longest name const menu = initFontNameMenu(selectedFont); // Toggle the fontselect popup on the select click fontSelect.onclick = () => { if (!se.browser.isSafari()) { se.range = se.getRange(); } if (menu.dataset.active === 'true') { menu.close(); } else { menu.open(); } }; // Set the select to initialize on the first font name selectedFont.textContent = '(inherited font)'; fontSelect.setAttribute('id', 'toolbar-fontName'); fontSelect.classList.add('se-toolbar-selection'); selectedFont.classList.add('se-toolbar-option'); fontSelect.append(selectedFont); se.toolbarOptionsGroup.append(fontSelect); se.fontName = selectedFont; } // Enable font size option if (enabledOptions.includes('fontSize')) { const fontSizeSelect = document.createElement('div'); const selectedFontSize = document.createElement('p'); const menu = initFontSizeMenu(selectedFontSize); fontSizeSelect.onclick = () => { if (!se.browser.isSafari()) { se.range = se.getRange(); } if (menu.dataset.active === 'true') { menu.close(); } else { menu.open(); } }; fontSizeSelect.setAttribute('id', 'toolbar-fontSize'); selectedFontSize.textContent = '(inherited size)'; fontSizeSelect.classList.add('se-toolbar-selection'); selectedFontSize.classList.add('se-toolbar-option'); fontSizeSelect.append(selectedFontSize); se.toolbarOptionsGroup.append(fontSizeSelect); se.fontSize = selectedFontSize; } if (enabledOptions.includes('fontFormat')) { const fontFormatSelect = document.createElement('div'); const selectedFontFormat = document.createElement('p'); const menu = initFontFormatMenu(); fontFormatSelect.onclick = () => { if (!se.browser.isSafari()) { se.range = se.getRange(); } if (menu.dataset.active === 'true') { menu.close(); } else { menu.open(); } }; fontFormatSelect.setAttribute('id', 'toolbar-fontFormat'); selectedFontFormat.textContent = 'Format'; fontFormatSelect.classList.add('se-toolbar-selection'); selectedFontFormat.classList.add('se-toolbar-option'); fontFormatSelect.append(selectedFontFormat); se.toolbarOptionsGroup.append(fontFormatSelect); } } /** * Initialized the StrivenEditor body * @returns {HTMLElement} The StrivenEditor body */ initBody() { const se = this; const body = document.createElement('div'); body.classList.add('se-body'); body.contentEditable = 'true'; body.style.height = se.editor.style.height; body.style.minHeight = se.editor.style.minHeight; body.style.maxHeight = se.editor.style.maxHeight; se.editor.setAttribute('height', se.editor.style.height); se.editor.setAttribute('min-height', se.editor.style.minHeight); se.editor.setAttribute('max-height', se.editor.style.maxHeight); se.editor.style.height = 'auto'; se.editor.style.minHeight = 'auto'; se.editor.style.maxHeight = 'auto'; se.options.placeholder && (body.dataset.placeholder = se.options.placeholder); se._bindContenteditableOnChange(body); // Execute this function on mouseup and keyup const execRange = () => { const current = se.getRange(); if (current) { se.range = current; } }; // Paste Handler body.onpaste = (e) => { // Editor Paste Handler if (se.options.onPaste) { const content = se.options.onPaste(e); if (content) { e.preventDefault(); se.executeCommand('insertHTML', content); return true; } } const afterPaste = () => { // After the paste setTimeout(() => { // Editor After Paste Handler se.options.afterPaste && se.options.afterPaste(e); }, 10); se.overflow(); }; // Paste image logic if (e.clipboardData.files && e.clipboardData.files.length > 0 ) { e.preventDefault(); se.insertImages(e.clipboardData.files).finally(() => { afterPaste(); }); return true; } // pasting text content if (e.clipboardData.items && e.clipboardData.items.length > 0 && e.clipboardData.items[0].type === 'text/plain') { let plainText = e.clipboardData.getData('text/plain'); if (se.validURL(plainText.trim())) { // get meta data if (se.options.metaUrl) { se.getMeta(plainText).then((res) => { const { url, title, image, description } = res; url && title && image && se.createMetaDataElement(url, image, title, description); }); } } } let pastedHTML = false; if (e.clipboardData.types.includes('text/html')) { //get the html pastedHTML = e.clipboardData.getData('text/html'); } // Wrap pasted link content for resetting the range if (pastedHTML) { e.preventDefault(); let pasteNode = document.createElement('span'); pasteNode.innerHTML = pastedHTML; //cleanup styles this.pruneInlineStyles(pasteNode); //cleanup css classes this.cleanCss(pasteNode); //sanitize if (se.options.sanitizePaste) { pasteNode = this.scrubHTML(pasteNode); } pasteNode.setAttribute('class', 'se-pasted-content'); se.executeCommand('insertHTML', pasteNode.innerHTML); } afterPaste(); }; body.onkeydown = (e) => { switch (e.key) { case 'Tab': if (se.options.canTab) { e.shiftKey ? se.executeCommand('outdent') : se.executeCommand('indent'); e.preventDefault(); } break; case 'Shift': break; case 'Backspace': se.textBuffer && (se.textBuffer = se.textBuffer.substring(0, se.textBuffer.length - 1)); break; case 'Control': break; case 'Semicolon': if (e.shiftKey) { se.textBuffer ? (se.textBuffer += ':') : (se.textBuffer = ':'); } break; default: se.textBuffer ? (se.textBuffer += e.key) : (se.textBuffer = e.key); break; } }; // State of the editor const bodyKeyup = body.onkeyup; body.onkeyup = (e) => { bodyKeyup && bodyKeyup(); execRange(); if (e.key) { switch (e.key) { case 'Enter': se.options.onEnter && se.options.onEnter(e); break; default: break; } } se.setFontStates(); se.toolbarState(); }; // addEventListener('keyup', (e) => { // const tab = (e.key === 'Tab' || e.keyCode === 9); // if (tab && document.activeElement === se.body) { // if (se.body.textContent.trim() === '') { // const r = se.getRange(); // if (r) { // const selNode = document.createElement('span'); // selNode.innerHTML = '&nbsp;'; // se.body.append(selNode); // r.selectNode(selNode); // r.collapse(); // } // } else { // const r = se.getRange(); // if (r) { // r.selectNodeContents(se.body); // r.collapse(); // } // } // } // if (tab && document.activeElement !== se.body && se.body.textContent.trim() === '') { // se.clearContent(); // } // }); const bodyFocus = body.onfocus; body.onfocus = (e) => { !se.browser.isEdge() && se.setRange(); window.addEventListener('mouseup', execRange); se.editor.classList.add('se-focus'); if (se.body.textContent.trim() === '') { const r = se.getRange(); if (r) { const selNode = document.createTextNode(''); se.body.append(selNode); setTimeout(() => { se.getRange().selectNode(selNode); selNode.remove(); }, 0); } } if (se.scrollPosition && !se.browser.isEdge()) { body.scrollTo(se.scrollPosition); } bodyFocus && bodyFocus(); }; // Bind state management event body.addEventListener('focus', () => { // disables all states se.options.toolbarOptions.forEach((opt) => { if (typeof opt === 'string') { if (document.queryCommandState('insertUnorderedList') && opt === 'insertUnorderedList') { return false; } if (document.queryCommandState('insertOrderedList') && opt === 'insertOrderedList') { return false; } document.queryCommandState(opt) && se.executeCommand(opt); } }); // enable only active states se.getActiveOptions().forEach((opt) => { !document.queryCommandState(opt) && se.executeCommand(opt); }); }); const bodyBlur = body.onblur; body.onblur = (e) => { se.editor.classList.remove('se-focus'); window.removeEventListener('mouseup', execRange); se.scrollPosition = { y: body.scrollTop, x: body.scrollWidth }; se.textBuffer = null; se.clearLinksToEdit(); se.clearImagesToEdit(); bodyBlur && bodyBlur(); }; const bodyClick = body.onclick; body.onclick = (event) => { se.closeAllMenus(); se.setFontStates(); body.textContent && se.toolbarState(); se.handleImageClick(event.target); bodyClick && bodyClick(); }; return body; } /** * Intializes the StrivenEditor link menu popup * @returns {HTMLElement} The StrivenEditor link menu */ initLinkMenu() { const se = this; const linkMenu = document.createElement('div'); const linkMenuHeader = document.createElement('p'); const linkMenuForm = document.createElement('div'); const linkMenuButtons = document.createElement('div'); const linkMenuButton = document.createElement('button'); const linkMenuCloseButton = document.createElement('button'); const linkMenuFormLabel = document.createElement('p'); const linkMenuFormInput = document.createElement('input'); const linkMenuCheck = document.createElement('input'); function resetInput() { linkMenuFormInput.value = 'http://'; } linkMenu.id = 'link-menu'; linkMenu.classList.add('se-popup', 'se-popup-top'); linkMenu.dataset.active = 'false'; linkMenuForm.classList.add('se-popup-form'); linkMenuFormLabel.classList.add('se-form-label'); linkMenuFormLabel.textContent = 'URL'; linkMenuFormInput.classList.add('se-form-input'); se.options.useBootstrap && linkMenuFormInput.classList.add('form-control'); linkMenuFormInput.type = 'text'; linkMenuFormInput.placeholder = 'Insert a Link'; resetInput(); linkMenuButton.type = 'button'; linkMenuCloseButton.type = 'button'; linkMenuButtons.classList.add('se-popup-button-container'); linkMenuButton.classList.add('se-popup-button', 'se-button-primary'); linkMenuButton.textContent = 'Insert'; linkMenuCloseButton.classList.add('se-popup-button', 'se-button-secondary'); linkMenuCloseButton.textContent = 'Close'; linkMenuButton.onclick = (e) => { const linkValue = linkMenuFormInput.value; se.body.focus(); se.setRange(); if (linkValue) { const linkToEdit = se.body.querySelector('.se-link-to-edit'); if (linkToEdit) { linkToEdit.setAttribute('href', linkValue); linkToEdit.innerText = textRowInput.value || linkValue; linkToEdit.classList.remove('se-link-to-edit'); linkToEdit.setAttribute('target', windowRowInput.checked ? '_blank' : ''); linkToEdit.setAttribute('contenteditable', true); se.makeLinksClickable([linkToEdit]); } else if (!se.body.textContent.trim()) { const linkToCreate = document.createElement('a'); linkToCreate.setAttribute('href', linkValue); linkToCreate.innerText = textRowInput.value || linkValue; linkToCreate.setAttribute('contenteditable', true); linkToCreate.setAttribute('target', windowRowInput.checked ? '_blank' : ''); se.body.append(linkToCreate); se.makeLinksClickable([linkToCreate]); } if (se.options.metaUrl && se.validURL(linkValue)) { se.getMeta(linkValue).then((res) => { const { url, image, title, description } = res; url && image && title && se.createMetaDataElement(url, image, title, description); }); } // trigger input event let inpEvent = new Event('input', { cancelable: true, bubbles: true }); se.body.dispatchEvent(inpEvent); se.closeLinkMenu(); } else { se.body.focus(); se.closeLinkMenu(); } resetInput(); }; linkMenuCloseButton.onclick = (e) => { se.body.focus(); se.closeLinkMenu(); resetInput(); }; linkMenuHeader.classList.add('se-popup-header'); linkMenuHeader.innerText = 'Insert Link'; linkMenu.appendChild(linkMenuHeader); linkMenuForm.appendChild(linkMenuFormLabel); linkMenuForm.appendChild(linkMenuFormInput); const textRow = linkMenuForm.cloneNode(true); const textRowLabel = textRow.querySelector('.se-form-label'); const textRowInput = textRow.querySelector('.se-form-input'); if (textRowLabel) { textRowLabel.innerText = 'Text'; } if (textRowInput) { textRowInput.value = ''; textRowInput.placeholder = 'Text content'; } const windowRow = linkMenuForm.cloneNode(true); const windowRowLabel = windowRow.querySelector('.se-form-label'); const windowRowInput = windowRow.querySelector('.se-form-input'); if (windowRow) { windowRow.setAttribute('style', 'justify-content: flex-end; align-items: center; flex-direction: row-reverse'); } if (windowRowLabel) { windowRowLabel.innerText = 'Open in new window'; windowRowLabel.style.marginLeft = '5px'; } if (windowRowInput) { windowRowInput.checked = true; windowRowInput.setAttribute('type', 'checkbox'); windowRowInput.setAttribute('style', 'width: auto'); } linkMenu.appendChild(linkMenuForm); linkMenu.appendChild(textRow); linkMenu.appendChild(windowRow); linkMenuButtons.appendChild(linkMenuButton); linkMenuButtons.appendChild(linkMenuCloseButton); linkMenu.appendChild(linkMenuButtons); [...linkMenu.getElementsByTagName('input')].forEach((inp) => { inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); } }; inp.onblur = () => { setTimeout(() => se.clearLinksToEdit(), 200); }; }); return linkMenu; } /** * Initializes the StrivenEditor image menu popup * @returns {HTMLElement} The StrivenEditor image menu */ initImageMenu() { const se = this; const imageMenu = document.createElement('div'); const imageMenuHeader = document.createElement('div'); // Upload tab button const imageMenuUploadTabButton = document.createElement('button'); imageMenuUploadTabButton.classList.add('se-tab-button', 'se-tab-button-upload', 'tab-button-active'); imageMenuUploadTabButton.type = 'button'; imageMenuUploadTabButton.tabIndex = 1; // Link tab button const imageMenuLinkTabButton = document.createElement('button'); imageMenuLinkTabButton.classList.add('se-tab-button', 'se-tab-button-link'); imageMenuLinkTabButton.type = 'button'; imageMenuUploadTabButton.tabIndex = 2; // Image Menu Tabs const imageMenuUploadTab = document.createElement('div'); imageMenuUploadTab.classList.add('se-image-menu-tab', 'se-image-menu-upload-tab'); const imageMenuLinkTab = document.createElement('div'); imageMenuLinkTab.classList.add('se-image-menu-tab', 'se-image-menu-link-tab'); imageMenuLinkTab.style.display = 'none'; // Upload form inputs const imageMenuUploadInput = document.createElement('input'); imageMenuUploadInput.type = 'file'; imageMenuUploadInput.accept = 'image/jpeg,image/webp,image/gif,image/png,image/svg+xml,image/bmp,image/x-icon'; imageMenuUploadInput.multiple = true; imageMenuUploadInput.style.display = 'none'; // Drop zone const imageMenuUploadDropZone = document.createElement('div'); imageMenuUploadDropZone.classList.add('se-file-drop-dropzone'); const imageMenuUploadDropZoneText = document.createElement('p'); imageMenuUploadDropZoneText.textContent = 'Click to upload OR drag and drop images here'; imageMenuUploadDropZone.appendChild(createSVG(UPLOADICON, undefined, '48px', '48px')); imageMenuUploadDropZone.appendChild(imageMenuUploadDropZoneText); // Link form inputs const imageMenuForm = document.createElement('div'); const imageMenuButtons = document.createElement('div'); const imageInsertButton = document.createElement('button'); const imageMenuCloseButton = document.createElement('button'); const imageMenuFormLabel = document.createElement('p'); const imageMenuFormSourceInput = document.createElement('input'); imageMenu.id = 'image-menu'; imageMenu.classList.add('se-popup', 'se-popup-top'); imageMenu.dataset.active = 'false'; imageMenuForm.classList.add('se-popup-form'); imageMenuFormLabel.classList.add('se-form-label'); imageMenuFormLabel.textContent = 'Image URL'; // Set up image URL input field imageMenuFormSourceInput.classList.add('se-form-input'); se.options.useBootstrap && imageMenuFormSourceInput.classList.add('form-control'); imageMenuFormSourceInput.type = 'text'; im