suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
274 lines (242 loc) • 8 kB
JavaScript
import { PluginModal } from '../../interfaces';
import { Modal, Controller } from '../../modules/contract';
import { ModalAnchorEditor } from '../../modules/ui';
import { dom, numbers } from '../../helper';
/**
* @typedef {Object} LinkOptions
* @property {string} [uploadUrl] - The URL endpoint for file uploads.
* - The server must return:
* ```js
* {
* "result": [
* {
* "url": "https://example.com/file.pdf",
* "name": "file.pdf",
* "size": 1048576
* }
* ]
* }
* ```
* @property {Object<string, string>} [uploadHeaders] - Additional headers for file upload requests.
* @property {number} [uploadSizeLimit] - The total file upload size limit in bytes.
* @property {number} [uploadSingleSizeLimit] - The single file upload size limit in bytes.
* @property {string} [acceptedFormats] - Accepted file formats for link uploads.
*/
/**
* @typedef {Omit<LinkOptions & import('../../modules/ui/ModalAnchorEditor').ModalAnchorEditorParams, ''>} LinkPluginOptions
*/
/**
* @class
* @description Link plugin.
* - This plugin provides link insertion and editing functionality within the editor.
* - It also supports file uploads if an upload URL is provided.
*/
class Link extends PluginModal {
static key = 'link';
static className = 'se-icon-flip-rtl';
#controllerATarget;
/**
* @constructor
* @param {SunEditor.Kernel} kernel - The Kernel instance
* @param {LinkPluginOptions} pluginOptions
*/
constructor(kernel, pluginOptions) {
// plugin bisic properties
super(kernel);
this.title = this.$.lang.link;
this.icon = 'link';
// define plugin options
pluginOptions.textToDisplay = true;
pluginOptions.title = true;
// create HTML
const modalEl = CreateHTML_modal(this.$);
const controllerEl = CreateHTML_controller(this.$);
// members
const uploadUrl = typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null;
this.target = null;
this.isUpdateState = false;
this.pluginOptions = {
...pluginOptions,
uploadUrl,
uploadHeaders: pluginOptions.uploadHeaders || null,
uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
acceptedFormats: typeof pluginOptions.acceptedFormats === 'string' ? pluginOptions.acceptedFormats.trim() : null,
enableFileUpload: !!uploadUrl,
};
// modules
this.anchor = new ModalAnchorEditor(this.$, modalEl, this.pluginOptions);
this.modal = new Modal(this, this.$, modalEl);
this.controller = new Controller(this, this.$, controllerEl, { position: 'bottom', disabled: false });
this.#controllerATarget = this.controller.form.querySelector('a');
}
/**
* @hook Editor.EventManager
* @type {SunEditor.Hook.Event.Active}
*/
active(element) {
if (dom.check.isAnchor(element) && !element.hasAttribute('data-se-non-link')) {
const a = this.#controllerATarget;
const href = element.getAttribute('href');
a.href = href;
a.textContent = a.title = element.textContent;
a.target = href?.startsWith('#') ? element.target : '_blank';
dom.utils.addClass(element, 'on');
this.anchor.set(element);
this.controller.open(element, null, { isWWTarget: false, initMethod: null, addOffset: null });
this.target = element;
return true;
}
this.controller.close();
return false;
}
/**
* @override
* @type {PluginModal['open']}
*/
open() {
this.modal.open();
}
/**
* @hook Modules.Modal
* @type {SunEditor.Hook.Modal.On}
*/
modalOn(isUpdate) {
this.isUpdateState = isUpdate;
this.anchor.on(isUpdate);
}
/**
* @hook Modules.Modal
* @type {SunEditor.Hook.Modal.Action}
*/
async modalAction() {
const oA = this.anchor.create(false);
if (oA === null) return false;
if (!this.isUpdateState) {
const selectedFormats = this.$.format.getLines();
if (selectedFormats.length > 1) {
if (!this.$.html.insertNode(dom.utils.createElement(selectedFormats[0].nodeName, null, oA), { afterNode: null, skipCharCount: false })) return true;
} else {
if (!this.$.html.insertNode(oA, { afterNode: null, skipCharCount: false })) return true;
}
this.$.selection.setRange(oA.childNodes[0], 0, oA.childNodes[0], oA.textContent.length);
} else {
// set range
const textNode = this.controller.currentTarget.childNodes[0];
this.$.selection.setRange(textNode, 0, textNode, textNode.textContent.length);
}
this.$.history.push(false);
return true;
}
/**
* @hook Modules.Modal
* @type {SunEditor.Hook.Modal.Init}
*/
modalInit() {
this.controller.close();
this.anchor.init();
}
/**
* @hook Modules.Controller
* @type {SunEditor.Hook.Controller.Action}
*/
controllerAction(target) {
const command = target.getAttribute('data-command');
if (/update/.test(command)) {
this.modal.open();
} else if (/copy/.test(command)) {
this.$.html.copy(this.target);
} else if (/unlink/.test(command)) {
const sc = dom.query.getEdgeChild(
this.controller.currentTarget,
function (current) {
return current.childNodes.length === 0 || current.nodeType === 3;
},
false,
);
const ec = dom.query.getEdgeChild(
this.controller.currentTarget,
function (current) {
return current.childNodes.length === 0 || current.nodeType === 3;
},
true,
);
this.$.selection.setRange(sc, 0, ec, ec.textContent.length);
this.$.inline.apply(null, { stylesToModify: null, nodesToRemove: ['A'], strictRemove: false });
} else {
/** delete */
dom.utils.removeItem(this.controller.currentTarget);
this.controller.currentTarget = null;
this.$.focusManager.focus();
this.$.history.push(false);
}
}
/**
* @hook Modules.Controller
* @type {SunEditor.Hook.Controller.Close}
*/
controllerClose() {
dom.utils.removeClass(this.controller.currentTarget, 'on');
}
}
/**
* @param {SunEditor.Deps} $ - Kernel dependencies
* @returns {HTMLElement}
*/
function CreateHTML_modal({ lang, icons }) {
const html = /*html*/ `
<form>
<div class="se-modal-header">
<button type="button" data-command="close" class="se-btn se-close-btn" title="${lang.close}" aria-label="${lang.close}">
${icons.cancel}
</button>
<span class="se-modal-title">${lang.link_modal_title}</span>
</div>
<div class="se-anchor-editor"></div>
<div class="se-modal-footer">
<button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}">
<span>${lang.submitButton}</span>
</button>
</div>
</form>`;
return dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
}
/**
* @param {SunEditor.Deps} $ - Kernel dependencies
* @returns {HTMLElement}
*/
function CreateHTML_controller({ lang, icons }) {
const html = /*html*/ `
<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-btn se-tooltip">
${icons.edit}
<span class="se-tooltip-inner">
<span class="se-tooltip-text">${lang.edit}</span>
</span>
</button>
<button type="button" data-command="copy" tabindex="-1" class="se-btn se-tooltip">
${icons.copy}
<span class="se-tooltip-inner">
<span class="se-tooltip-text">${lang.copy}</span>
</span>
</button>
<button type="button" data-command="unlink" tabindex="-1" class="se-btn se-tooltip se-icon-flip-rtl">
${icons.unlink}
<span class="se-tooltip-inner">
<span class="se-tooltip-text">${lang.unlink}</span>
</span>
</button>
<button type="button" data-command="delete" tabindex="-1" class="se-btn se-tooltip">
${icons.delete}
<span class="se-tooltip-inner">
<span class="se-tooltip-text">${lang.remove}</span>
</span>
</button>
</div>
</div>`;
return dom.utils.createElement('DIV', { class: 'se-controller se-controller-link' }, html);
}
export default Link;