/** You can use "dialog" and "component", "fileManager", "resizing" modules */ /** An example of using the module can be seen in the 4.dialog sample. */ // import dialog from '/src/plugins/modules/[dialog, resizing]'; // <script src="https://cdn.jsdelivr.net/npm/suneditor@latest/src/plugins/modules/[dialog, resizing].js"></script> // SUNEDITOR_MODULES['dialog', 'resizing'] /** ----------------------------------------------------------------------- */ /** These are the free icon sites you can use */ // --svg // https://icons8.com/ // https://icon-icons.com // https://materialdesignicons.com // https://material.io/resources/icons/?style=baseline // https://www.freepik.com/ // --icon class // https://fontawesome.com/, https://www.jsdelivr.com/package/npm/@fortawesome/fontawesome-free // <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.8.2/css/all.min.css"> /** ----------------------------------------------------------------------- */ // Define custom plugin // Common properties var customPlugin = { // @Required @Unique name: 'custom_example', // @Required display: ('container' || 'command' || 'submenu' || 'dialog'), // @options // * You can also set from the button list // HTML title attribute (tooltip) - default: plugin's name title: 'Custom tooltip', // HTML to be append to button (icon) // Recommend using the inline svg icon. - default: "<span class="se-icon-text">!</span>" innerHTML: '<svg />, <i class="" />, <span class="se-icon-text">C</span>', // The class of the button. - default: "se-btn" // "se-code-view-enabled": It is not disable when on code view mode. // "se-resizing-enabled": It is not disable when on using resizing module. buttonClass: '', // @Required add: (core, targetElement) { // How to set language when setting button properties of plugin directly in plugin const titleList = { en: 'Custom', ko: '사용자 정의', } this.title = titleList[core.lang.code] }, ... } SUNEDITOR.create(document.getElementById('ex_custom'), { // ------ When using CDN plugins: [customPlugin], // ------ When using node.js plugins: [custom_container, plugins.blockquote, plugins.link], // --- all plguins plugins: { ...plugins, custom_container }, // ------ Add button list // --- Add the name of the plugin to the button list. // --- Button settings use the contents defined in the plugin. buttonList: [ [ 'custom_example' ] ] // --- You can set the button's properties directly. buttonList: [ [ { // plugin's name attribute // It must be the same as the name attribute of the plugin name: 'custom_example', // Enter the "display" attribute value of your custom plugin. dataDisplay: ('container' || 'command' || 'submenu' || 'dialog'), // @options // HTML title attribute title: 'Custom plugin', // button's class ("se-btn" class is registered, basic button click css is applied.) buttonClass:'', // ------ HTML to be append to button // --- Inline svg (The default size of the svg file is 16px.(suneditor.css:54L)) innerHTML:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width:24px;height:24px;"><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>' // --- Icon class (This icon uses Font Awesome) innerHTML:'<i class="fas fa-font"></i>' } ] ] })1. Plugins container
// ex) Adds a container of type "submenu" that contains a plugin. var custom_container = { // @Required @Unique // plugin name name: 'custom_container', // @Required // data display display: 'container', // @Required // add function - It is called only once when the plugin is first run. // This function generates HTML to append and register the event. // arguments - (core : core object, targetElement : clicked button element) add: function (core, targetElement) { // @Required // Registering a namespace for caching as a plugin name in the context object const context = core.context; context.custom_container = {}; // Generate submenu HTML // Always bind "core" when calling a plugin function let listDiv = this.setSubmenu(core); // You must bind "core" object when registering an event. /** add event listeners */ listDiv.querySelector('.se-form-group').addEventListener('click', this.onClick.bind(core)); // @Required // You must add the "submenu" element using the "core.initMenuTarget" method. /** append target button menu */ core.initMenuTarget(this.name, targetElement, listDiv); }, setSubmenu: function (core) { const listDiv = core.util.createElement('DIV'); const icons = core.icons; // assets/defaultIcons.js listDiv.className = 'se-menu-container se-submenu se-list-layer'; listDiv.innerHTML = '' + '<div class="se-list-inner">' + '<div class="se-form-group">' + // @Required // The "position" style of each element surrounding the button must be "relative". // suneditor.css: .sun-editor .se-form-group > div {position:relative;} '<div>' + // @Required // Enter the button name of the plug-in or default command in the button's "data-command" '<button type="button" class="se-btn se-tooltip" data-command="bold" style="margin: 0 !important;">' + icons.bold + '<span class="se-tooltip-inner">' + '<span class="se-tooltip-text">Quote</span>' + '</span>' + '</button>' + '</div>' + '<div>' + '<button type="button" class="se-btn se-tooltip" data-command="blockquote">' + icons.blockquote + '<span class="se-tooltip-inner">' + '<span class="se-tooltip-text">Quote</span>' + '</span>' + '</button>' + '</div>' + '<div>' + '<button type="button" class="se-btn se-tooltip" data-command="link">' + icons.link + '<span class="se-tooltip-inner">' + '<span class="se-tooltip-text">Link</span>' + '</span>' + '</button>' + '</div>' + '<div>' + '<button type="button" class="se-btn se-tooltip" data-command="table">' + icons.table + '<span class="se-tooltip-inner">' + '<span class="se-tooltip-text">Table</span>' + '</span>' + '</button>' + '</div>' + '<div>' + '<button type="button" class="se-btn se-tooltip" data-command="textStyle">' + icons.text_style + '<span class="se-tooltip-inner">' + '<span class="se-tooltip-text">Text style</span>' + '</span>' + '</button>' + '</div>' + '</div>' + '</div>'; return listDiv; }, onClick: function (e) { e.preventDefault(); e.stopPropagation(); let target = e.target; let command = ''; while (!command && !/^UL$/i.test(target.tagName)) { command = target.getAttribute('data-command'); if (command) break; target = target.parentNode; } if (!command) return; const plugin = this.plugins[command]; this.actionCall(command, (plugin ? plugin.display : ''), target); } };SUNEDITOR.create(document.getElementById('ex_container'), { plugins: [custom_container], buttonList: [ [ { name: 'custom_container', dataDisplay:'container', title:'custom_container', buttonClass:'', innerHTML:'<svg viewBox="0 0 24 24" style="width:24px;height:24px;"><path fill="currentColor" d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" /></svg>' } ] ] });2. Command
// ex) A command plugin to add "Range format element(util.isRangeFormatElement)" to selection var plugin_command = { // @Required @Unique // plugin name name: 'customCommand', // @Required // data display display: 'command', // @Options title: 'Add range tag', buttonClass: '', innerHTML: '<i class="fas fa-carrot"></i>', // @Required // add function - It is called only once when the plugin is first run. // This function generates HTML to append and register the event. // arguments - (core : core object, targetElement : clicked button element) add: function (core, targetElement) { const context = core.context; const rangeTag = core.util.createElement('div'); core.util.addClass(rangeTag, '__se__format__range_custom'); // @Required // Registering a namespace for caching as a plugin name in the context object context.customCommand = { targetButton: targetElement, tag: rangeTag }; }, // @Override core // Plugins with active methods load immediately when the editor loads. // Called each time the selection is moved. active: function (element) { if (!element) { this.util.removeClass(this.context.customCommand.targetButton, 'active'); } else if (this.util.hasClass(element, '__se__format__range_custom')) { this.util.addClass(this.context.customCommand.targetButton, 'active'); return true; } return false; }, // @Required, @Override core // The behavior of the "command plugin" must be defined in the "action" method. action: function () { const rangeTag = this.util.getRangeFormatElement(this.getSelectionNode()); if (this.util.hasClass(rangeTag, '__se__format__range_custom')) { this.detachRangeFormatElement(rangeTag, null, null, false, false); } else { this.applyRangeFormatElement(this.context.customCommand.tag.cloneNode(false)); } } }// ex) A command plugin to add "text node" to selection var plugin_command_2 = { name: 'customCommand_2', display: 'command', title:'Text node change', buttonClass:'', // This icon uses Font Awesome innerHTML:'<i class="fas fa-font"></i>', add: function (core, targetElement) { const context = core.context; context.customCommand_2 = { targetButton: targetElement }; }, active: function (element) { if (!element) { this.util.removeClass(this.context.customCommand_2.targetButton, 'active'); } else if (/^mark$/i.test(element.nodeName) && element.style.backgroundColor.length > 0) { this.util.addClass(this.context.customCommand_2.targetButton, 'active'); return true; } return false; }, action: function () { if (!this.util.hasClass(this.context.customCommand_2.targetButton, 'active')) { const newNode = this.util.createElement('MARK'); newNode.style.backgroundColor = 'hsl(60,75%,60%)'; this.nodeChange(newNode, ['background-color'], null, null); } else { this.nodeChange(null, ['background-color'], ['mark'], true); } } }SUNEDITOR.create(document.getElementById('ex_command'), { plugins: [plugin_command, plugin_command_2], buttonList: [ ['customCommand', 'customCommand_2'] ] });3. Submenu
// ex) A submenu plugin that appends the contents of the input element to the editor var plugin_submenu = { // @Required @Unique // plugin name name: 'custom_plugin_submenu', // @Required // data display display: 'submenu', // @Options title: 'Custom plugin of the submenu', buttonClass: '', innerHTML: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>', // @Required // add function - It is called only once when the plugin is first run. // This function generates HTML to append and register the event. // arguments - (core : core object, targetElement : clicked button element) add: function (core, targetElement) { // @Required // Registering a namespace for caching as a plugin name in the context object const context = core.context; context.customSubmenu = { targetButton: targetElement, textElement: null, currentSpan: null }; // Generate submenu HTML // Always bind "core" when calling a plugin function let listDiv = this.setSubmenu(core); // Input tag caching context.customSubmenu.textElement = listDiv.querySelector('input'); // You must bind "core" object when registering an event. /** add event listeners */ listDiv.querySelector('.se-btn-primary').addEventListener('click', this.onClick.bind(core)); listDiv.querySelector('.se-btn').addEventListener('click', this.onClickRemove.bind(core)); // @Required // You must add the "submenu" element using the "core.initMenuTarget" method. /** append target button menu */ core.initMenuTarget(this.name, targetElement, listDiv); }, setSubmenu: function (core) { const listDiv = core.util.createElement('DIV'); // @Required // A "se-submenu" class is required for the top level element. listDiv.className = 'se-menu-container se-submenu se-list-layer'; listDiv.innerHTML = '' + '<div class="se-list-inner">' + '<ul class="se-list-basic" style="width: 230px;">' + '<li>' + '<div class="se-form-group">' + '<input class="se-input-form" type="text" placeholder="insert text" style="border: 1px solid #CCC;" />' + '<button type="button" class="se-btn-primary se-tooltip">' + '<strong>OK</strong>' + '<span class="se-tooltip-inner">' + '<span class="se-tooltip-text">Append span</span>' + '</span>' + '</button>' + '<button type="button" class="se-btn se-tooltip">' + '<strong>X</strong>' + '<span class="se-tooltip-inner">' + '<span class="se-tooltip-text">Remove</span>' + '</span>' + '</button>' + '</div>' + '</li>' + '</ul>' + '</div>'; return listDiv; }, // @Override core // Plugins with active methods load immediately when the editor loads. // Called each time the selection is moved. active: function (element) { // If no tag matches, the "element" argument is called with a null value. if (!element) { this.util.removeClass(this.context.customSubmenu.targetButton, 'active'); this.context.customSubmenu.textElement.value = ''; this.context.customSubmenu.currentSpan = null; } else if (this.util.hasClass(element, 'se-custom-tag')) { this.util.addClass(this.context.customSubmenu.targetButton, 'active'); this.context.customSubmenu.textElement.value = element.textContent; this.context.customSubmenu.currentSpan = element; return true; } return false; }, // @Override submenu // Called after the submenu has been rendered on: function () { this.context.customSubmenu.textElement.focus(); }, onClickRemove: function () { const span = this.context.customSubmenu.currentSpan; if (span) { this.util.removeItem(span); this.context.customSubmenu.currentSpan = null; this.submenuOff(); this.focus(); } }, onClick: function () { const value = this.context.customSubmenu.textElement.value.trim(); if (!value) return; const span = this.context.customSubmenu.currentSpan; if (span) { span.textContent = value; this.setRange(span, 1, span, 1); } else { this.functions.insertHTML('<span class="se-custom-tag">' + value + '</span>', true); this.context.customSubmenu.textElement.value = ''; } this.submenuOff(); } };SUNEDITOR.create(document.getElementById('ex_submenu'), { plugins: [plugin_submenu], buttonList: [ ['custom_plugin_submenu',] ] });4. Dialog
// Import "dialog" module // import dialog from '../../src/plugins/modules/dialog'; <script src="https://cdn.jsdelivr.net/npm/suneditor@latest/src/plugins/modules/dialog.js"></script> // ex) ex) A link dialog plugin with multiple target options. var plugin_dialog = { // @Required // plugin name name: 'customLink', // @Required // data display display: 'dialog', // @Required // add function - It is called only once when the plugin is first run. // This function generates HTML to append and register the event. // arguments - (core : core object, targetElement : clicked button element) add: function (core) { // If you are using a module, you must register the module using the "addModule" method. core.addModule([SUNEDITOR_MODULES.dialog]); // @Required // Registering a namespace for caching as a plugin name in the context object const context = core.context; context.customLink = { focusElement: null, // @Override // This element has focus when the dialog is opened. targetSelect: null, linkAnchorText: null, _linkAnchor: null }; /** link dialog */ let link_dialog = this.setDialog(core); context.customLink.modal = link_dialog; context.customLink.focusElement = link_dialog.querySelector('._se_link_url'); context.customLink.linkAnchorText = link_dialog.querySelector('._se_link_text'); context.customLink.targetSelect = link_dialog.querySelector('.se-input-select'); /** link controller */ let link_controller = this.setController_LinkButton(core); context.customLink.linkController = link_controller; context.customLink._linkAnchor = null; /** add event listeners */ link_dialog.querySelector('form').addEventListener('submit', this.submit.bind(core)); link_controller.addEventListener('click', this.onClick_linkController.bind(core)); /** append html */ context.dialog.modal.appendChild(link_dialog); /** append controller */ context.element.relative.appendChild(link_controller); /** empty memory */ link_dialog = null, link_controller = null; }, /** dialog */ setDialog: function (core) { const lang = core.lang; const dialog = core.util.createElement('DIV'); const targetList = [ { target: '_blank', name: 'New window'}, { target: '_parent', name: 'Parent frame'}, { target: '_top', name: 'First frame', selected: true}, { target: 'AnyFrame', name: 'Frame name'}, { target: '_dialog', name: 'Self defined dialog'} ]; dialog.className = 'se-dialog-content'; dialog.style.display = 'none'; let html = '' + '<form class="editor_link">' + '<div class="se-dialog-header">' + '<button type="button" data-command="close" class="se-btn se-dialog-close" aria-label="Close" title="' + lang.dialogBox.close + '">' + core.icons.cancel + '</button>' + '<span class="se-modal-title">' + lang.dialogBox.linkBox.title + '</span>' + '</div>' + '<div class="se-dialog-body">' + '<div class="se-dialog-form">' + '<label>' + lang.dialogBox.linkBox.url + '</label>' + '<input class="se-input-form _se_link_url" type="text" />' + '</div>' + '<div class="se-dialog-form">' + '<label>' + lang.dialogBox.linkBox.text + '</label><input class="se-input-form _se_link_text" type="text" />' + '</div>' + '<div class="se-dialog-form se-dialog-form-footer">' + '<select class="se-input-select" title="links">'; for (let i = 0, len = targetList.length, t, selected; i < len; i++) { t = targetList[i]; selected = t.selected ? ' selected' : ''; html += '<option value="' + t.target + '"' + selected + '>' + t.name + '</option>'; } html += '</select>' + '</div>' + '</div>' + '<div class="se-dialog-footer">' + '<button type="submit" class="se-btn-primary" title="' + lang.dialogBox.submitButton + '"><span>' + lang.dialogBox.submitButton + '</span></button>' + '</div>' + '</form>'; dialog.innerHTML = html; return dialog; }, /** modify controller button */ setController_LinkButton: function (core) { const lang = core.lang; const icons = core.icons; const link_btn = core.util.createElement('DIV'); link_btn.className = 'se-controller se-controller-link'; link_btn.innerHTML = '' + '<div class="se-arrow se-arrow-up"></div>' + '<div class="link-content"><span><a target="_blank" href=""></a> </span>' + '<div class="se-btn-group">' + '<button type="button" data-command="update" tabindex="-1" class="se-tooltip">' + icons.edit + '<span class="se-tooltip-inner"><span class="se-tooltip-text">' + lang.controller.edit + '</span></span>' + '</button>' + '<button type="button" data-command="unlink" tabindex="-1" class="se-tooltip">' + icons.unlink + '<span class="se-tooltip-inner"><span class="se-tooltip-text">' + lang.controller.unlink + '</span></span>' + '</button>' + '<button type="button" data-command="delete" tabindex="-1" class="se-tooltip">' + icons.delete + '<span class="se-tooltip-inner"><span class="se-tooltip-text">' + lang.controller.remove + '</span></span>' + '</button>' + '</div>' + '</div>'; return link_btn; }, // @Required, @Override dialog // This method is called when the plugin button is clicked. // Open the modal window here. open: function () { // open.call(core, pluginName, isModify) this.plugins.dialog.open.call(this, 'customLink', 'customLink' === this.currentControllerName); }, submit: function (e) { this.showLoading(); e.preventDefault(); e.stopPropagation(); const submitAction = function () { if (this.context.customLink.focusElement.value.trim().length === 0) return false; const contextLink = this.context.customLink; const url = contextLink.focusElement.value; const anchor = contextLink.linkAnchorText; const anchorText = anchor.value.length === 0 ? url : anchor.value; // When opened for modification "this.context.dialog.updateModal" is true if (!this.context.dialog.updateModal) { const oA = this.util.createElement('A'); oA.href = url; oA.textContent = anchorText; oA.target = contextLink.targetSelect.selectedOptions[0].value; const selectedFormats = this.getSelectedElements(); if (selectedFormats.length > 1) { const oFormat = this.util.createElement(selectedFormats[0].nodeName); oFormat.appendChild(oA); this.insertNode(oFormat); } else { this.insertNode(oA); } this.setRange(oA.childNodes[0], 0, oA.childNodes[0], oA.textContent.length); } else { contextLink._linkAnchor.href = url; contextLink._linkAnchor.textContent = anchorText; contextLink._linkAnchor.target = contextLink.targetSelect.selectedOptions[0].value; // set range this.setRange(contextLink._linkAnchor.childNodes[0], 0, contextLink._linkAnchor.childNodes[0], contextLink._linkAnchor.textContent.length); } // history stack this.history.push(false); contextLink.focusElement.value = ''; contextLink.linkAnchorText.value = ''; }.bind(this); try { submitAction(); } finally { this.plugins.dialog.close.call(this); this.closeLoading(); this.focus(); } return false; }, // @Override core // Plugins with active methods load immediately when the editor loads. // Called each time the selection is moved. active: function (element) { if (!element) { if (this.controllerArray.indexOf(this.context.customLink.linkController) > -1) { this.controllersOff(); } } else if (this.util.isAnchor(element) && element.getAttribute('data-image-link') === null) { if (this.controllerArray.indexOf(this.context.customLink.linkController) < 0) { this.plugins.customLink.call_controller.call(this, element); } return true; } return false; }, // @Override dialog // This method is called just before the dialog opens. // If "update" argument is true, it is not a new call, but a call to modify an already created element. on: function (update) { if (!update) { this.plugins.customLink.init.call(this); this.context.customLink.linkAnchorText.value = this.getSelection().toString(); } else if (this.context.customLink._linkAnchor) { // "update" and "this.context.dialog.updateModal" are always the same value. // This code is an exception to the "link" plugin. this.context.dialog.updateModal = true; this.context.customLink.focusElement.value = this.context.customLink._linkAnchor.href; this.context.customLink.linkAnchorText.value = this.context.customLink._linkAnchor.textContent; this.context.customLink.targetSelect.value = this.context.customLink._linkAnchor.target || ''; } }, call_controller: function (selectionATag) { this.editLink = this.context.customLink._linkAnchor = selectionATag; const linkBtn = this.context.customLink.linkController; const link = linkBtn.querySelector('a'); link.href = selectionATag.href; link.title = selectionATag.textContent; link.textContent = selectionATag.textContent; const offset = this.util.getOffset(selectionATag, this.context.element.wysiwygFrame); linkBtn.style.top = (offset.top + selectionATag.offsetHeight + 10) + 'px'; linkBtn.style.left = (offset.left - this.context.element.wysiwygFrame.scrollLeft) + 'px'; linkBtn.style.display = 'block'; const overLeft = this.context.element.wysiwygFrame.offsetWidth - (linkBtn.offsetLeft + linkBtn.offsetWidth); if (overLeft < 0) { linkBtn.style.left = (linkBtn.offsetLeft + overLeft) + 'px'; linkBtn.firstElementChild.style.left = (20 - overLeft) + 'px'; } else { linkBtn.firstElementChild.style.left = '20px'; } // Show controller at editor area (controller elements, function, "controller target element(@Required)", "controller name(@Required)", etc..) this.controllersOn(linkBtn, selectionATag, 'customLink'); }, onClick_linkController: function (e) { e.stopPropagation(); const command = e.target.getAttribute('data-command'); if (!command) return; e.preventDefault(); if (/update/.test(command)) { const contextLink = this.context.customLink; contextLink.focusElement.value = contextLink._linkAnchor.href; contextLink.linkAnchorText.value = contextLink._linkAnchor.textContent; contextLink.targetSelect.value = contextLink.targetSelect.value; this.plugins.dialog.open.call(this, 'customLink', true); } else if (/unlink/.test(command)) { const sc = this.util.getChildElement(this.context.customLink._linkAnchor, function (current) { return current.childNodes.length === 0 || current.nodeType === 3; }, false); const ec = this.util.getChildElement(this.context.customLink._linkAnchor, function (current) { return current.childNodes.length === 0 || current.nodeType === 3; }, true); this.setRange(sc, 0, ec, ec.textContent.length); this.nodeChange(null, null, ['A'], false); } else { /** delete */ this.util.removeItem(this.context.customLink._linkAnchor); this.context.customLink._linkAnchor = null; this.focus(); // history stack this.history.push(false); } this.controllersOff(); }, // @Required, @Override dialog // This method is called when the dialog window is closed. // Initialize the properties. init: function () { const contextLink = this.context.customLink; contextLink.linkController.style.display = 'none'; contextLink._linkAnchor = null; contextLink.focusElement.value = ''; contextLink.linkAnchorText.value = ''; contextLink.targetSelect.selectedIndex = 0; } };SUNEDITOR.create(document.getElementById('ex_dialog'), { plugins: [plugin_dialog], buttonList: [ [ { name: 'customLink', dataDisplay:'dialog', title:'Custom link', buttonClass:'', innerHTML:'<svg viewBox="0 0 24 24"><path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></svg>' } ] ] });5. Dialog & component & fileManager
Audio list
<script src="https://cdn.jsdelivr.net/npm/suneditor@latest/src/plugins/modules/dialog.js"></script> <script src="https://cdn.jsdelivr.net/npm/suneditor@latest/src/plugins/modules/component.js"></script> <script src="https://cdn.jsdelivr.net/npm/suneditor@latest/src/plugins/modules/fileManager.js"></script> <!-- <script src="https://cdn.jsdelivr.net/npm/suneditor@latest/src/plugins/modules/resizing.js"></script> --> <div class="component-list"> <div class="file-list-info"> <span>Audio list</span> </div> <div class="component-file-list"> <ul id="audio_list"></ul> </div> </div>/** import modules when using nodejs */ // import dialog from 'suneditor/src/plugins/modules/dialog'; // import component from 'suneditor/src/plugins/modules/component'; // import fileManager from 'suneditor/src/plugins/modules/fileManager'; // import resizing from 'suneditor/src/plugins/modules/resizing'; // import { dialog, component, fileManager, resizing } from 'suneditor/src/plugins/modules'; /** audio list */ const audioTable = document.getElementById('audio_list'); let audioList = []; function userFunc_audioUpload (targetElement, index, state, info, remainingFilesCount) { console.log('audioInfo', info); if (state === 'delete') { audioList.splice(findIndex(audioList, index), 1) } else { if (state === 'create') { audioList.push(info) } else { // update // } } if (remainingFilesCount === 0) { console.log('audioList', audioList) _setAudioList(audioList) } } function _setAudioList () { let list = ''; for (let i = 0, info; i < audioList.length; i++) { info = audioList[i]; list += '<li>' + '<button title="delete" onclick="_selectAudio(\'delete\',' + info.index + ')">X</button>' + '<a href="javascript:void(0)" onclick="_selectAudio(\'select\',' + info.index + ')">' + info.src + '</a>' + '</li>'; } audioTable.innerHTML = list; } function _selectAudio (type, index) { audioList[findIndex(audioList, index)][type](); }// ex) A link dialog plugin with used [dialog, component, fileManager] module // Sample audio : https://file-examples.com/index.php/sample-audio-files/ var plugin_dialog_component_fileManager = { /** * @Required @Unique * plugin name */ name: 'customAudio', /** * @Required * data display */ display: 'dialog', /** * @options * You can also set from the button list */ title:'Custom audio', buttonClass:'', innerHTML:'<span class="se-icon-text">?</span>', /** * @Required * add function - It is called only once when the plugin is first run. * This function generates HTML to append and register the event. * arguments - (core : core object, targetElement : clicked button element) */ add: function (core) { // If you are using a module, you must register the module using the "addModule" method. core.addModule([dialog, component, fileManager]); /** * @Required * Registering a namespace for caching as a plugin name in the context object */ const context = core.context; context.customAudio = { _infoList: [], // @Override fileManager _infoIndex: 0, // @Override fileManager _uploadFileLength: 0, // @Override fileManager focusElement: null, // @Override // This element has focus when the dialog is opened. targetSelect: null, // @require @Override component _element: null, _cover: null, _container: null, }; // buton title const titleList = { en: 'Audio', ko: '오디오' }; core.title = titleList[core.lang.code]; // languages const customAudioLang = { en: { title: 'Audio', file: 'Select from files', url: 'Audio url' }, ko: { title: '오디오', file: '파일에서 선택', url: '오디오 주소' } }; core.lang.audio = customAudioLang[core.lang.code]; /** dialog */ let audio_dialog = this.setDialog(core); context.customAudio.modal = audio_dialog; context.customAudio.fileInput = audio_dialog.querySelector('._se_audio_files'); context.customAudio.urlInput = audio_dialog.querySelector('.se-input-url'); context.customAudio.focusElement = context.customAudio.fileInput; /** controller */ let audio_controller = this.setController(core); context.customAudio.controller = audio_controller; /** add event listeners */ audio_dialog.querySelector('.se-dialog-files-edge-button').addEventListener('click', this._removeSelectedFiles.bind(context.fileInput, context.urlInput)); audio_dialog.querySelector('form').addEventListener('submit', this.submit.bind(core)); audio_controller.addEventListener('click', this.onClick_controller.bind(core)); /** append html */ context.dialog.modal.appendChild(audio_dialog); /** append controller */ context.element.relative.appendChild(audio_controller); /** empty memory */ audio_dialog = null, audio_controller = null; }, /** HTML - dialog */ setDialog: function (core) { const lang = core.lang; const dialog = core.util.createElement('DIV'); dialog.className = 'se-dialog-content'; dialog.style.display = 'none'; let html = '' + '<form class="editor_link">' + '<div class="se-dialog-header">' + '<button type="button" data-command="close" class="se-btn se-dialog-close" aria-label="Close" title="' + lang.dialogBox.close + '">' + core.icons.cancel + '</button>' + '<span class="se-modal-title">' + lang.audio.title + '</span>' + '</div>' + '<div class="se-dialog-body">' + '<div class="se-dialog-form">' + '<label>' + lang.audio.file + '</label>' + '<div class="se-dialog-form-files">' + '<input class="se-input-form _se_audio_files" type="file" accept="audio/*" multiple="multiple" />' + '<button type="button" data-command="filesRemove" class="se-btn se-dialog-files-edge-button" title="' + lang.controller.remove + '">' + core.icons.cancel + '</button>' + '</div>' + '</div>' + '<div class="se-dialog-form">' + '<label>' + lang.audio.url + '</label>' + '<input class="se-input-form se-input-url" type="text" />' + '</div>' + '</div>' + '<div class="se-dialog-footer">' + '<button type="submit" class="se-btn-primary" title="' + lang.dialogBox.submitButton + '"><span>' + lang.dialogBox.submitButton + '</span></button>' + '</div>' + '</form>'; dialog.innerHTML = html; return dialog; }, /** HTML - controller */ setController: function (core) { const lang = core.lang; const icons = core.icons; const link_btn = core.util.createElement('DIV'); link_btn.className = 'se-controller se-controller-link'; link_btn.innerHTML = '' + '<div class="se-arrow se-arrow-up"></div>' + '<div class="link-content">' + '<div class="se-btn-group">' + '<button type="button" data-command="update" tabindex="-1" class="se-tooltip">' + icons.edit + '<span class="se-tooltip-inner"><span class="se-tooltip-text">' + lang.controller.edit + '</span></span>' + '</button>' + '<button type="button" data-command="delete" tabindex="-1" class="se-tooltip">' + icons.delete + '<span class="se-tooltip-inner"><span class="se-tooltip-text">' + lang.controller.remove + '</span></span>' + '</button>' + '</div>' + '</div>'; return link_btn; }, // Disable url input when uploading files _removeSelectedFiles: function (urlInput) { this.value = ''; if (urlInput) urlInput.removeAttribute('disabled'); }, /** * @Required @Override fileManager */ fileTags: ['audio'], /** * @Override core, fileManager, resizing * @description It is called from core.selectComponent. * @param {Element} element Target element */ select: function (element) { this.plugins.customAudio.onModifyMode.call(this, element); }, /** * @Override fileManager, resizing * @param {Element} element Target element */ destroy: function (element) { element = element || this.context.customAudio._element; const container = this.util.getParentElement(element, this.util.isComponent) || element; const dataIndex = element.getAttribute('data-index') * 1; const focusEl = (container.previousElementSibling || container.nextElementSibling); const emptyDiv = container.parentNode; this.util.removeItem(container); this.plugins.customAudio.init.call(this); this.controllersOff(); if (emptyDiv !== this.context.element.wysiwyg) this.util.removeItemAllParents(emptyDiv, function (current) { return current.childNodes.length === 0; }, null); // focus this.focusEdge(focusEl); // fileManager event // (pluginName, data-index, "uploadEventHandler") this.plugins.fileManager.deleteInfo.call(this, 'customAudio', dataIndex, userFunc_audioUpload); // history stack this.history.push(false); }, /** * @Override fileManager */ checkFileInfo: function () { // (pluginName, [tag], "uploadEventHandler", "formatFixFunction", "using resizing module?") this.plugins.fileManager.checkInfo.call(this, 'customAudio', ['audio'], userFunc_audioUpload, this.plugins.customAudio.updateCover.bind(this), false); }, /** * @Override fileManager */ resetFileInfo: function () { // (pluginName, data-index, "uploadEventHandler") this.plugins.fileManager.resetInfo.call(this, 'customAudio', userFunc_audioUpload); }, /** * @Required @Override dialog * This method is called just before the dialog opens. * @param {Boolean} update If "update" argument is true, it is not a new call, but a call to modify an already created element. */ on: function (update) { if (!update) { this.plugins.customAudio.init.call(this); } else if (this.context.customAudio._element) { // "update" and "this.context.dialog.updateModal" are always the same value. // This code is an exception to the "link" plugin. this.context.dialog.updateModal = true; this.context.customAudio.urlInput.value = this.context.customAudio._element.src; } }, /** * @Required @Override dialog * This method is called when the plugin button is clicked. * Open the modal window here. */ open: function () { // open.call(core, pluginName, isModify) this.plugins.dialog.open.call(this, 'customAudio', 'customAudio' === this.currentControllerName); }, submit: function (e) { const context = this.context.customAudio; e.preventDefault(); e.stopPropagation(); try { if (context.fileInput.files.length > 0) { // upload files this.plugins.customAudio.submitAction.call(this, context.fileInput.files); } else if (context.urlInput.value.trim().length > 0) { // url this.plugins.customAudio.setupUrl.call(this, context.urlInput); } } catch (error) { throw Error('[SUNEDITOR.audio.submit.fail] cause : "' + error.message + '"'); } finally { this.plugins.dialog.close.call(this); } return false; }, submitAction: function (fileList) { if (fileList.length === 0) return; let fileSize = 0; const files = []; for (let i = 0, len = fileList.length; i < len; i++) { if (/audio/i.test(fileList[i].type)) { files.push(fileList[i]); fileSize += fileList[i].size; } } const context = this.context.customAudio; const audioPlugin = this.plugins.customAudio; context._uploadFileLength = files.length; const filesLen = this.context.dialog.updateModal ? 1 : files.length; const info = { isUpdate: this.context.dialog.updateModal, element: context._element }; // create formData const formData = new FormData(); for (let i = 0; i < filesLen; i++) { formData.append('file-' + i, files[i]); } // fileManager - upload // (uploadURL, uploadHeader, formData, callBack, errorCallBack) this.plugins.fileManager.upload.call(this, 'http://localhost:3000', {}, formData, audioPlugin.callBack_upload.bind(this, info), audioPlugin.callBack_error); }, callBack_upload: function (info, xmlHttp) { const response = JSON.parse(xmlHttp.responseText); if (response.errorMessage) { this.functions.noticeOpen(response.errorMessage); } else { const fileList = response.result; let oAudio = null; if (info.isUpdate) { oAudio = info.element; } else { oAudio = this.util.createElement('AUDIO'); oAudio.setAttribute('controls', true); } for (let i = 0, len = fileList.length, file; i < len; i++) { file = { name: fileList[i].name, size: fileList[i].size }; this.plugins.customAudio.create_audio.call(this, oAudio, fileList[i].url, file, info.isUpdate); } } }, callBack_error: function (errorMessage, response, core) { core.functions.noticeOpen(errorMessage | response.toString()); }, setupUrl: function () { try { this.showLoading(); const context = this.context.customAudio; const src = context.urlInput.value.trim(); if (src.length === 0) return false; const oAudio = this.util.createElement('AUDIO'); oAudio.setAttribute('controls', true); // When opened for modification "this.context.dialog.updateModal" is true this.plugins.customAudio.create_audio.call(this, oAudio, src, null, this.context.dialog.updateModal); } catch (error) { throw Error('[SUNEDITOR.audio.audio.fail] cause : "' + error.message + '"'); } finally { this.closeLoading(); } }, // create or update create_audio: function (element, src, file, isUpdate) { const context = this.context.customAudio; // create new tag if (!isUpdate) { element.src = src; // In order to use it in the form of components such as images and videos, // you need to create component tags by calling the "set_cover" and "set_container" functions of the "component" module. const cover = this.plugins.component.set_cover.call(this, element); const container = this.plugins.component.set_container.call(this, cover, ''); this.insertComponent(container, false); } // update else if (element && element.src !== src) { element = context._element; element.src = src } // not changed else { return; } // call fileManager.setInfo when updated tag // (pluginName, element, "uploadEventHandler", file, "using resizing module") this.plugins.fileManager.setInfo.call(this, 'customAudio', element, userFunc_audioUpload, file, false); this.history.push(false); }, // Update container for "audio" tag not matching format to be used in "checkFileInfo" updateCover: function (element) { const context = this.context.customAudio; element.setAttribute('controls', true); // find component element const existElement = this.util.getParentElement(element, this.util.isMediaComponent) || this.util.getParentElement(element, function (current) { return this.isWysiwygDiv(current.parentNode); }.bind(this.util)); // clone element context._element = element = element.cloneNode(false); const cover = this.plugins.component.set_cover.call(this, element); const container = this.plugins.component.set_container.call(this, cover, 'se-video-container'); existElement.parentNode.replaceChild(container, existElement); // call fileManager.setInfo when updated tag // (pluginName, element, "uploadEventHandler", file, "using resizing module") this.plugins.fileManager.setInfo.call(this, 'customAudio', element, userFunc_audioUpload, null, false); }, /** * @Required @Override fileManager, resizing * @param {Element} selectionTag Selected element * @param {Object} size Size object{w, h, t, 1} of "core.plugins.resizing.call_controller_resize" return value when if using "resizing" module */ onModifyMode: function (selectionTag) { const context = this.context.customAudio; const controller = context.controller; const offset = this.util.getOffset(selectionTag, this.context.element.wysiwygFrame); controller.style.top = (offset.top + selectionTag.offsetHeight + 10) + 'px'; controller.style.left = (offset.left - this.context.element.wysiwygFrame.scrollLeft) + 'px'; controller.style.display = 'block'; const overLeft = this.context.element.wysiwygFrame.offsetWidth - (controller.offsetLeft + controller.offsetWidth); if (overLeft < 0) { controller.style.left = (controller.offsetLeft + overLeft) + 'px'; controller.firstElementChild.style.left = (20 - overLeft) + 'px'; } else { controller.firstElementChild.style.left = '20px'; } // Show controller at editor area (controller elements, function, "controller target element(@Required)", "controller name(@Required)", etc..) this.controllersOn(controller, selectionTag, this.plugins.customAudio.init.bind(this), 'customAudio'); // set modify mode context selectionTag.style.border = '1px solid #80bdff'; context._element = selectionTag; context._cover = this.util.getParentElement(selectionTag, 'FIGURE'); context._container = this.util.getParentElement(selectionTag, this.util.isComponent); }, /** * @Required @Override fileManager, resizing */ openModify: function (notOpen) { this.context.customAudio.urlInput.value = this.context.customAudio._element.src; if (!notOpen) this.plugins.dialog.open.call(this, 'customAudio', true); }, onClick_controller: function (e) { e.stopPropagation(); const command = e.target.getAttribute('data-command'); if (!command) return; e.preventDefault(); if (/update/.test(command)) { this.plugins.customAudio.openModify.call(this, false); } else { /** delete */ this.plugins.customAudio.destroy.call(this, this.context.customAudio._element); } this.controllersOff(); }, /** * @Required @Override dialog * This method is called when the dialog window is closed. * Initialize the properties. */ init: function () { if (this.context.dialog.updateModal) return; const context = this.context.customAudio; if (context._element) context._element.style.border = ''; context.controller.style.display = 'none'; context._element = null; context.fileInput.value = ''; context.urlInput.value = ''; } };SUNEDITOR.create(document.getElementById('ex_dialog_component_fileManager'), { plugins: [plugin_dialog_component_fileManager], buttonList: [ [ 'customAudio', 'preview' ] ] });