UNPKG

jy-simple-editor

Version:

JY Editor membantu dalam penulisan, penataan gaya tulisan (styling), dan penyisipan rumus atau formula, jelas, dan mudah digunakan

340 lines (280 loc) 10.9 kB
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 = '&nbsp;'; dummy.setAttribute('data-spacer', 'true'); editorElement.appendChild(dummy); editorElement.appendChild(span); } else { editorElement.appendChild(span); } const spacer = document.createElement('span'); spacer.innerHTML = '&nbsp;'; 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 }; } export { JYeditor, bindToolbar };