/** 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' ]
]
});