jy-simple-editor
Version:
JY Editor membantu dalam penulisan, penataan gaya tulisan (styling), dan penyisipan rumus atau formula, jelas, dan mudah digunakan
349 lines (287 loc) • 12.3 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JYEditor = {}));
})(this, (function (exports) { 'use strict';
class Node {
constructor(value, prev = null, next = null) {
this.value = value;
this.prev = prev;
this.next = next;
}
}
class EditorHistory {
constructor() {
this.head = null;
this.tail = null;
this.current = null;
}
addState(content) {
const newNode = new Node(content);
if (this.current) {
newNode.prev = this.current;
this.current.next = newNode;
} else {
this.head = newNode;
}
this.current = newNode;
this.tail = newNode;
}
undo() {
if (this.current && this.current.prev) {
this.current = this.current.prev;
return this.current.value;
}
return null;
}
redo() {
if (this.current && this.current.next) {
this.current = this.current.next;
return this.current.value;
}
return null;
}
canUndo() {
return this.current && this.current.prev !== null;
}
canRedo() {
return this.current && this.current.next !== null;
}
}
function createEditorLayout() {
return `
<div class="min-w-md w-full max-w-md mx-auto relative">
<div class="watermark p-1 border-[1px] rounded-md shadow-lg flex gap-2 text-xs items-center w-max absolute bottom-0 translate-y-1/2 right-2 z-10 text-gray-800 bg-gray-50">
<p>Created By </p>
<img src="assets/logo.svg" class="h-5" alt="">
<p>JY</p>
</div>
<div id="math-modal" style="display:none; position: absolute; top: 50px; right: 10px; background: white; border: 1px solid black;" class="rounded-lg p-4 w-80">
<h4>Masukkan Formula LaTeX:</h4>
<input type="text" id="math-input" class="w-full mb-4 p-2 rounded-md border-[1px] mt-1">
<div id="math-preview" class="min-h-12 mb-5 p-2 rounded-md border-[1px] border-dashed"></div>
<button type="button" id="insert-math" class="cursor-pointer bg-[#FFF86F] p-2 rounded-md">Tambah</button>
<button type="button" id="cancel-math" class="cursor-pointer bg-red-500 p-2 rounded-md text-white">Batal</button>
</div>
<div class="rounded-lg border-[1px] overflow-hidden bg-white">
<div class="toolbar w-full flex justify-between border-b-[1px]">
<div class="flex gap-2">
<button type="button" data-action="undo" class="p-2 rounded-md cursor-pointer"><img src="assets/undo.svg" alt=""></button>
<button type="button" data-action="redo" class="p-2 rounded-md cursor-pointer"><img src="assets/redo.svg" alt=""></button>
</div>
<div class="flex gap-2">
<div class="flex gap-2">
<button type="button" data-action="bold" class="p-2 rounded-md cursor-pointer"><img src="assets/b.svg" alt=""></button>
<button type="button" data-action="italic" class="p-2 rounded-md cursor-pointer"><img src="assets/i.svg" alt=""></button>
<button type="button" data-action="underline" class="p-2 rounded-md cursor-pointer"><img src="assets/u.svg" alt=""></button>
</div>
<div class="w-[1px] h-full bg-black"></div>
<div class="flex gap-2">
<button type="button" data-action="math" class="p-2 rounded-md cursor-pointer"><img src="assets/math.svg" alt=""></button>
</div>
</div>
</div>
<div id="jy-editor-content" contenteditable="true" class="editor w-full min-h-80 p-4"></div>
</div>
</div>
`;
}
function injectAssets() {
if (!document.querySelector('link[href*="output.css"]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/jy-simple-editor@latest/dist/output.css';
document.head.appendChild(link);
}
if (!document.querySelector('#MathJax-script')) {
const script = document.createElement('script');
script.id = 'MathJax-script';
script.async = true;
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
document.head.appendChild(script);
}
}
function placeCaretAfterNode(node) {
const selection = window.getSelection();
const range = document.createRange();
range.setStartAfter(node);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
function selectionIntersectsMathType() {
const selection = window.getSelection();
if (!selection.rangeCount) {
return false;
}
const range = selection.getRangeAt(0);
const treeWalker = document.createTreeWalker(
range.commonAncestorContainer,
NodeFilter.SHOW_ELEMENT,
{
acceptNode: function (node) {
if (range.intersectsNode(node) && node.getAttribute && node.getAttribute('contenteditable') === 'false') {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
}
}
);
return !!treeWalker.nextNode();
}
function applyStyle(styleType) {
if (selectionIntersectsMathType()) {
alert('Tidak bisa menerapkan style yang melewati MathType.');
return;
}
const commandMap = {
bold: 'bold',
italic: 'italic',
underline: 'underline'
};
const command = commandMap[styleType];
if (command) {
document.execCommand(command, false, null);
}
}
function initMathModal(editorElement) {
const mathModal = document.getElementById('math-modal');
const mathInput = document.getElementById('math-input');
const mathPreview = document.getElementById('math-preview');
const insertBtn = document.getElementById('insert-math');
const cancelBtn = document.getElementById('cancel-math');
document.querySelector('[data-action="math"]').onclick = () => {
mathInput.value = '';
mathPreview.innerHTML = '';
mathModal.style.display = 'block';
mathInput.focus();
};
mathInput.addEventListener('input', () => {
const rawValue = mathInput.value;
const latexValue = rawValue.replace(/ /g, ' \\ ');
mathPreview.innerHTML = `\\(${latexValue}\\)`;
if (window.MathJax) {
MathJax.typeset();
}
});
insertBtn.onclick = () => {
const rawValue = mathInput.value.trim();
if (rawValue) {
const latexValue = rawValue.replace(/ /g, ' \\ ');
const span = document.createElement('span');
span.innerText = `\\(${latexValue}\\)`;
span.contentEditable = 'false';
if (editorElement.innerHTML.trim() === '') {
const dummy = document.createElement('span');
dummy.innerHTML = ' ';
dummy.setAttribute('data-spacer', 'true');
editorElement.appendChild(dummy);
editorElement.appendChild(span);
} else {
editorElement.appendChild(span);
}
const spacer = document.createElement('span');
spacer.innerHTML = ' ';
spacer.setAttribute('data-spacer', 'true');
editorElement.appendChild(spacer);
if (window.MathJax) {
MathJax.typeset();
}
placeCaretAfterNode(spacer);
mathModal.style.display = 'none';
}
};
cancelBtn.onclick = () => {
mathModal.style.display = 'none';
};
}
function bindToolbar(editorInstance) {
document.querySelector('[data-action="bold"]').onclick = () => editorInstance.bold();
document.querySelector('[data-action="italic"]').onclick = () => editorInstance.italic();
document.querySelector('[data-action="underline"]').onclick = () => editorInstance.underline();
document.querySelector('[data-action="undo"]').onclick = () => editorInstance.undo();
document.querySelector('[data-action="redo"]').onclick = () => editorInstance.redo();
document.querySelector('[data-action="math"]').onclick = () => editorInstance.math();
}
class JYeditor {
constructor(textareaId) {
injectAssets();
const originalTextarea = document.getElementById(textareaId);
if (!originalTextarea || originalTextarea.tagName.toLowerCase() !== 'textarea') {
throw new Error(`Element with ID "${textareaId}" must be a <textarea>`);
}
const initialValue = originalTextarea.value;
const container = document.createElement('div');
container.innerHTML = createEditorLayout();
originalTextarea.parentNode.insertBefore(container, originalTextarea);
originalTextarea.style.display = 'none';
this.textarea = originalTextarea;
this.editor = container.querySelector('#jy-editor-content');
this.editor.innerHTML = initialValue;
this.history = new EditorHistory();
this.saveState();
this.editor.addEventListener('input', () => this.saveState());
initMathModal(this.editor);
bindToolbar(this);
}
saveState() {
this.history.addState(this.editor.innerHTML);
this.updateToolbarState();
}
bold() {
applyStyle('bold');
this.saveState();
}
italic() {
applyStyle('italic');
this.saveState();
}
underline() {
applyStyle('underline');
this.saveState();
}
undo() {
const prevState = this.history.undo();
if (prevState !== null) {
this.editor.innerHTML = prevState;
this.updateToolbarState();
}
}
redo() {
const nextState = this.history.redo();
if (nextState !== null) {
this.editor.innerHTML = nextState;
this.updateToolbarState();
}
}
math() {
initMathModal(this.editor);
}
getValue() {
return this.editor.innerHTML;
}
syncToTextarea() {
if (this.textarea) {
this.textarea.value = this.getValue();
}
}
updateToolbarState() {
const undoButton = document.querySelector('[data-action="undo"]');
const redoButton = document.querySelector('[data-action="redo"]');
if (undoButton) {
undoButton.disabled = !this.history.canUndo();
}
if (redoButton) {
redoButton.disabled = !this.history.canRedo();
}
}
}
if (typeof window !== 'undefined' && !window.JYEditor) {
window.JYEditor = {
JYeditor,
bindToolbar
};
}
exports.JYeditor = JYeditor;
exports.bindToolbar = bindToolbar;
}));