(function () { 'use strict'; const LANGUAGES = [ 'python', 'javascript', 'typescript', 'java', 'c', 'cpp', 'csharp', 'php', 'ruby', 'go', 'rust', 'swift', 'kotlin', 'scala', 'r', 'matlab', 'julia', 'perl', 'bash', 'shell', 'powershell', 'sql', 'html', 'css', 'scss', 'less', 'xml', 'json', 'yaml', 'toml', 'markdown', 'latex', 'dart', 'elixir', 'erlang', 'haskell', 'lua', 'objective-c', 'fortran', 'assembly', 'vhdl', 'verilog', 'graphql', 'dockerfile', 'nginx', 'apache' ]; class FancyMiku { constructor(container, options = {}) { this.container = typeof container === 'string' ? document.querySelector(container) : container; this.options = { height: options.height || '500px', placeholder: options.placeholder || 'Start writing...', onChange: options.onChange || null }; this.savedSelection = null; this.isSourceMode = false; this.currentPopup = null; this.init(); } init() { this.container.innerHTML = ''; this.createEditor(); this.attachEventListeners(); } createEditor() { const wrapper = document.createElement('div'); wrapper.className = 'miku-container'; const toolbar = this.createToolbar(); wrapper.appendChild(toolbar); const editableArea = document.createElement('div'); editableArea.className = 'miku-content'; editableArea.contentEditable = 'true'; editableArea.setAttribute('data-placeholder', this.options.placeholder); editableArea.style.height = this.options.height; editableArea.innerHTML = '


'; wrapper.appendChild(editableArea); const sourceArea = document.createElement('textarea'); sourceArea.className = 'miku-source'; sourceArea.style.height = this.options.height; sourceArea.style.display = 'none'; wrapper.appendChild(sourceArea); this.container.appendChild(wrapper); this.wrapper = wrapper; this.toolbar = toolbar; this.editableArea = editableArea; this.sourceArea = sourceArea; } createToolbar() { const toolbar = document.createElement('div'); toolbar.className = 'miku-toolbar'; const icons = { h1: ``, h2: ``, h3: ``, bold: ``, italic: ``, underline: ``, strikethrough: ``, link: ``, image: ``, blockquote: ``, codeblock: ``, inlinecode: ``, ul: ``, ol: ``, indent: ``, outdent: ``, source: `` } const buttons = [ { icon: icons.h1, title: 'Heading 1', command: 'heading', value: 'h1' }, { icon: icons.h2, title: 'Heading 2', command: 'heading', value: 'h2' }, { icon: icons.h3, title: 'Heading 3', command: 'heading', value: 'h3' }, { separator: true }, { icon: icons.bold, title: 'Bold', command: 'bold' }, { icon: icons.italic, title: 'Italic', command: 'italic' }, { icon: icons.underline, title: 'Underline', command: 'underline' }, { icon: icons.strikethrough, title: 'Strikethrough', command: 'strikethrough' }, { separator: true }, { icon: icons.link, title: 'Insert Link', command: 'link' }, { icon: icons.image, title: 'Insert Image', command: 'image' }, { separator: true }, { icon: icons.ul, title: 'Bullet List', command: 'insertUnorderedList' }, { icon: icons.ol, title: 'Numbered List', command: 'insertOrderedList' }, { icon: icons.indent, title: 'Indent', command: 'indent' }, { icon: icons.outdent, title: 'Outdent', command: 'outdent' }, { separator: true }, { icon: icons.blockquote, title: 'Blockquote', command: 'blockquote' }, { icon: icons.codeblock, title: 'Code Block', command: 'codeblock' }, { icon: icons.inlinecode, title: 'Inline Code', command: 'inlinecode' }, { separator: true }, { icon: icons.source, title: 'Toggle Source', command: 'togglesource', special: true } ]; buttons.forEach(btn => { if (btn.separator) { const separator = document.createElement('span'); separator.className = 'miku-toolbar-separator'; toolbar.appendChild(separator); } else { const button = document.createElement('button'); button.type = 'button'; button.className = 'miku-btn'; if (btn.special) button.classList.add('miku-btn-special'); button.innerHTML = btn.icon; button.title = btn.title; button.dataset.command = btn.command; if (btn.value) button.dataset.value = btn.value; toolbar.appendChild(button); } }); return toolbar; } attachEventListeners() { this.toolbar.addEventListener('click', (e) => { const btn = e.target.closest('.miku-btn'); if (!btn) return; e.preventDefault(); this.executeCommand(btn.dataset.command, btn.dataset.value); }); this.editableArea.addEventListener('mouseup', () => this.saveSelection()); this.editableArea.addEventListener('keyup', () => { this.saveSelection(); this.handleLinkEditing(); }); this.editableArea.addEventListener('click', (e) => this.handleLinkClick(e)); this.editableArea.addEventListener('keydown', (e) => this.handleKeyDown(e)); this.editableArea.addEventListener('input', () => { this.ensureParagraphStructure(); if (this.options.onChange) { this.options.onChange(this.getContent()); } }); this.editableArea.addEventListener('paste', (e) => this.handlePaste(e)); this.editableArea.addEventListener('blur', () => { if (this.editableArea.innerHTML.trim() === '') { this.editableArea.innerHTML = '


'; } }); document.addEventListener('click', (e) => { if (this.currentPopup && !this.currentPopup.contains(e.target) && !e.target.closest('.miku-btn')) { this.closePopup(); } }); } saveSelection() { const sel = window.getSelection(); if (sel.rangeCount > 0) { this.savedSelection = sel.getRangeAt(0); } } restoreSelection() { if (this.savedSelection) { const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(this.savedSelection); } } executeCommand(command, value = null) { if (command === 'togglesource') { this.toggleSource(); return; } if (this.isSourceMode) { alert('Switch to WYSIWYG mode to use formatting commands.'); return; } this.restoreSelection(); this.editableArea.focus(); switch (command) { case 'heading': this.formatHeading(value); break; case 'bold': document.execCommand('bold', false, null); break; case 'italic': document.execCommand('italic', false, null); break; case 'underline': document.execCommand('underline', false, null); break; case 'strikethrough': document.execCommand('strikethrough', false, null); break; case 'link': this.showLinkPopup(); return; case 'image': this.showImagePopup(); return; case 'insertUnorderedList': document.execCommand('insertUnorderedList', false, null); break; case 'insertOrderedList': document.execCommand('insertOrderedList', false, null); break; case 'indent': this.handleIndent(true); break; case 'outdent': this.handleIndent(false); break; case 'blockquote': this.insertBlockquote(); break; case 'codeblock': this.insertCodeBlock(); return; case 'inlinecode': this.insertInlineCode(); break; } this.saveSelection(); } toggleSource() { if (this.isSourceMode) { this.isSourceMode = false; this.setContent(this.sourceArea.value); this.editableArea.style.display = 'block'; this.sourceArea.style.display = 'none'; } else { this.sourceArea.value = this.getContent(); this.isSourceMode = true; this.editableArea.style.display = 'none'; this.sourceArea.style.display = 'block'; } } formatHeading(tag) { const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const parent = range.commonAncestorContainer.parentElement; if (parent.tagName && parent.tagName.match(/^H[1-6]$/)) { const p = document.createElement('p'); p.innerHTML = parent.innerHTML; parent.replaceWith(p); } else { document.execCommand('formatBlock', false, tag); } } showLinkPopup(existingLink = null) { this.closePopup(); this.saveSelection(); const popup = document.createElement('div'); popup.className = 'miku-popup'; popup.innerHTML = `
${existingLink ? 'Edit Link' : 'Insert Link'}
`; document.body.appendChild(popup); this.currentPopup = popup; const rect = this.wrapper.getBoundingClientRect(); popup.style.top = `${rect.top + 100}px`; popup.style.left = `${rect.left + (rect.width / 2) - 200}px`; popup.querySelector('.miku-popup-close').onclick = () => this.closePopup(); popup.querySelector('#link-cancel').onclick = () => this.closePopup(); popup.querySelector('#link-insert').onclick = () => { const text = popup.querySelector('#link-text').value; const url = popup.querySelector('#link-url').value; const target = popup.querySelector('#link-target').checked ? '_blank' : '_self'; if (existingLink) { existingLink.textContent = text; existingLink.href = url; existingLink.target = target; } else { this.insertLink(text, url, target); } this.closePopup(); }; popup.querySelector('#link-text').focus(); } insertLink(text, url, target) { if (!text || !url) return; const link = document.createElement('a'); link.href = url; link.target = target; link.textContent = text; link.className = 'miku-editable-link'; this.restoreSelection(); const selection = window.getSelection(); if (selection.rangeCount === 0) return; const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(link); range.setStartAfter(link); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } handleLinkClick(e) { if (e.target.tagName === 'A' && e.target.classList.contains('miku-editable-link')) { e.preventDefault(); const editIcon = document.createElement('span'); editIcon.className = 'miku-link-edit'; editIcon.innerHTML = '✎'; editIcon.onclick = (event) => { event.stopPropagation(); this.showLinkPopup(e.target); editIcon.remove(); }; e.target.parentNode.insertBefore(editIcon, e.target.nextSibling); setTimeout(() => editIcon.remove(), 3000); } } handleLinkEditing() { document.querySelectorAll('.miku-link-edit').forEach(icon => icon.remove()); } handleKeyDown(e) { const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const node = range.startContainer; const parent = node.nodeType === Node.TEXT_NODE ? node.parentElement : node; if (parent.closest('pre[contenteditable="true"]')) { if (e.key === 'Tab') { e.preventDefault(); document.execCommand('insertText', false, ' '); } return; } if (e.key === 'Tab') { const li = parent.closest('li'); if (li) { e.preventDefault(); if (e.shiftKey) { document.execCommand('outdent', false, null); } else { document.execCommand('indent', false, null); } return; } } if (e.key === 'Enter') { const code = parent.closest('code'); if (code && !code.closest('pre')) { e.preventDefault(); const textNode = document.createTextNode('\u00A0'); code.parentNode.insertBefore(textNode, code.nextSibling); const br = document.createElement('br'); textNode.parentNode.insertBefore(br, textNode.nextSibling); const newRange = document.createRange(); newRange.setStartAfter(br); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); return; } const blockquote = parent.closest('blockquote'); if (blockquote && !e.shiftKey) { e.preventDefault(); const p = document.createElement('p'); p.innerHTML = '
'; blockquote.insertAdjacentElement('afterend', p); const newRange = document.createRange(); newRange.setStart(p, 0); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); } } if (e.key === 'ArrowRight') { const code = parent.closest('code'); if (code && !code.closest('pre')) { const isAtEnd = range.endOffset === node.length; if (isAtEnd) { e.preventDefault(); const textNode = document.createTextNode('\u00A0'); if (code.nextSibling) { code.parentNode.insertBefore(textNode, code.nextSibling); } else { code.parentNode.appendChild(textNode); } const newRange = document.createRange(); newRange.setStart(textNode, 1); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); } } } if (e.key === 'ArrowLeft') { const code = parent.closest('code'); if (code && !code.closest('pre')) { const isAtStart = range.startOffset === 0; if (isAtStart) { e.preventDefault(); const textNode = document.createTextNode('\u00A0'); code.parentNode.insertBefore(textNode, code); const newRange = document.createRange(); newRange.setStart(textNode, 0); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); } } } if (e.key === 'Backspace') { if (range.collapsed && range.startOffset === 0) { const prevSibling = parent.previousElementSibling; if (prevSibling && prevSibling.classList.contains('miku-code-wrapper')) { e.preventDefault(); prevSibling.remove(); } } } if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { setTimeout(() => { const sel = window.getSelection(); if (sel.rangeCount > 0) { const r = sel.getRangeAt(0); const n = r.startContainer; const p = n.nodeType === Node.TEXT_NODE ? n.parentElement : n; const code = p.closest('code'); if (code && !code.closest('pre')) { const textNode = document.createTextNode('\u00A0'); if (e.key === 'ArrowDown') { if (code.nextSibling) { code.parentNode.insertBefore(textNode, code.nextSibling); } else { code.parentNode.appendChild(textNode); } } else { code.parentNode.insertBefore(textNode, code); } const newRange = document.createRange(); newRange.setStart(textNode, 0); newRange.collapse(true); sel.removeAllRanges(); sel.addRange(newRange); } } }, 0); } } showImagePopup() { this.closePopup(); this.saveSelection(); const popup = document.createElement('div'); popup.className = 'miku-popup'; popup.innerHTML = `
Insert Image
`; document.body.appendChild(popup); this.currentPopup = popup; const rect = this.wrapper.getBoundingClientRect(); popup.style.top = `${rect.top + 100}px`; popup.style.left = `${rect.left + (rect.width / 2) - 200}px`; popup.querySelector('.miku-popup-close').onclick = () => this.closePopup(); popup.querySelector('#image-cancel').onclick = () => this.closePopup(); popup.querySelector('#image-insert').onclick = () => { const url = popup.querySelector('#image-url').value; const display = popup.querySelector('#image-display').value; if (url && url !== 'https://') { const link = document.createElement('a'); link.href = url; link.target = '_blank'; const img = document.createElement('img'); img.src = url; img.alt = 'Image'; if (display === 'block') img.className = 'block'; link.appendChild(img); this.restoreSelection(); const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(link); range.setStartAfter(link); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } else { this.editableArea.appendChild(link); } this.closePopup(); } }; popup.querySelector('#image-url').focus(); } handleIndent(increase) { const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const node = range.startContainer; const parent = node.nodeType === Node.TEXT_NODE ? node.parentElement : node; if (parent.closest('li')) { document.execCommand(increase ? 'indent' : 'outdent', false, null); return; } const block = parent.closest('p, h1, h2, h3, h4, h5, h6'); if (block) { const current = parseInt(block.style.marginLeft || '0', 10); const step = 24; const newMargin = increase ? current + step : Math.max(0, current - step); block.style.marginLeft = newMargin > 0 ? newMargin + 'px' : ''; } } insertBlockquote() { const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const parent = range.commonAncestorContainer.parentElement; if (parent.closest('blockquote')) { const blockquote = parent.closest('blockquote'); const p = document.createElement('p'); p.innerHTML = blockquote.innerHTML; blockquote.replaceWith(p); } else { document.execCommand('formatBlock', false, 'blockquote'); } } insertCodeBlock() { this.closePopup(); const codeWrapper = document.createElement('div'); codeWrapper.className = 'miku-code-wrapper'; codeWrapper.contentEditable = 'false'; const langSelect = document.createElement('select'); langSelect.className = 'miku-code-lang'; langSelect.innerHTML = LANGUAGES.map(lang => `` ).join(''); const pre = document.createElement('pre'); pre.setAttribute('data-language', 'python'); pre.contentEditable = 'true'; pre.textContent = '// Your code here...'; langSelect.onchange = () => { pre.setAttribute('data-language', langSelect.value); }; codeWrapper.appendChild(langSelect); codeWrapper.appendChild(pre); this.restoreSelection(); const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(codeWrapper); const p = document.createElement('p'); p.innerHTML = '
'; codeWrapper.insertAdjacentElement('afterend', p); setTimeout(() => { pre.focus(); const newRange = document.createRange(); newRange.selectNodeContents(pre); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(newRange); }, 50); } else { this.editableArea.appendChild(codeWrapper); const p = document.createElement('p'); p.innerHTML = '
'; this.editableArea.appendChild(p); setTimeout(() => pre.focus(), 50); } } insertInlineCode() { const selection = window.getSelection(); const selectedText = selection.toString(); if (selectedText) { const code = document.createElement('code'); code.textContent = selectedText; const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(code); } else { const code = document.createElement('code'); code.textContent = 'code'; this.insertNodeAtCursor(code); } } insertNodeAtCursor(node) { const selection = window.getSelection(); if (!selection.rangeCount) { this.editableArea.appendChild(node); return; } const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(node); range.setStartAfter(node); range.setEndAfter(node); selection.removeAllRanges(); selection.addRange(range); } ensureParagraphStructure() { const children = Array.from(this.editableArea.childNodes); children.forEach(child => { if (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== '') { const p = document.createElement('p'); p.textContent = child.textContent; child.replaceWith(p); } }); if (this.editableArea.children.length === 0) { const p = document.createElement('p'); p.innerHTML = '
'; this.editableArea.appendChild(p); } } handlePaste(e) { e.preventDefault(); const text = e.clipboardData.getData('text/plain'); const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); range.deleteContents(); const node = range.startContainer; const parentBlock = node.nodeType === Node.TEXT_NODE ? node.parentElement.closest('p, h1, h2, h3, h4, h5, h6, blockquote') : node.closest('p, h1, h2, h3, h4, h5, h6, blockquote'); const lines = text.split('\n'); if (parentBlock && parentBlock !== this.editableArea) { const afterText = range.startContainer.nodeType === Node.TEXT_NODE ? range.startContainer.textContent.slice(range.startOffset) : ''; if (range.startContainer.nodeType === Node.TEXT_NODE) { range.startContainer.textContent = range.startContainer.textContent.slice(0, range.startOffset); } let insertAfter = parentBlock; if (lines.length === 1) { const textNode = document.createTextNode(lines[0]); if (range.startContainer.nodeType === Node.TEXT_NODE) { range.startContainer.parentNode.insertBefore(textNode, range.startContainer.nextSibling); } else { range.insertNode(textNode); } if (afterText) { const afterNode = document.createTextNode(afterText); textNode.parentNode.insertBefore(afterNode, textNode.nextSibling); } const newRange = document.createRange(); newRange.setStartAfter(textNode); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); return; } parentBlock.textContent = (parentBlock.textContent.slice(0, range.startOffset) || '') ; const firstTextNode = document.createTextNode(lines[0]); parentBlock.appendChild(firstTextNode); if (!parentBlock.textContent.trim()) parentBlock.innerHTML = '
'; for (let i = 1; i < lines.length; i++) { const p = document.createElement('p'); if (i === lines.length - 1 && afterText) { p.textContent = (lines[i] || '') + afterText; } else { p.textContent = lines[i] || '\u00A0'; } insertAfter.insertAdjacentElement('afterend', p); insertAfter = p; } const newRange = document.createRange(); newRange.selectNodeContents(insertAfter); newRange.collapse(false); selection.removeAllRanges(); selection.addRange(newRange); } else { let lastNode = null; lines.forEach((line) => { const p = document.createElement('p'); p.textContent = line || '\u00A0'; if (lastNode) { lastNode.insertAdjacentElement('afterend', p); } else { range.insertNode(p); } lastNode = p; }); if (lastNode) { const newRange = document.createRange(); newRange.selectNodeContents(lastNode); newRange.collapse(false); selection.removeAllRanges(); selection.addRange(newRange); } } } closePopup() { if (this.currentPopup) { this.currentPopup.remove(); this.currentPopup = null; } } getContent() { if (this.isSourceMode) { return this.sourceArea.value; } const clone = this.editableArea.cloneNode(true); clone.querySelectorAll('.miku-code-wrapper').forEach(wrapper => { const pre = wrapper.querySelector('pre'); const cleanPre = pre.cloneNode(true); cleanPre.removeAttribute('contenteditable'); wrapper.replaceWith(cleanPre); }); clone.querySelectorAll('.miku-link-edit').forEach(el => el.remove()); return clone.innerHTML; } setContent(html) { if (this.isSourceMode) { this.sourceArea.value = html; } else { this.editableArea.innerHTML = html || '


'; this.editableArea.querySelectorAll('pre[data-language]').forEach(pre => { if (!pre.parentElement.classList.contains('miku-code-wrapper')) { const wrapper = document.createElement('div'); wrapper.className = 'miku-code-wrapper'; wrapper.contentEditable = 'false'; const langSelect = document.createElement('select'); langSelect.className = 'miku-code-lang'; langSelect.innerHTML = LANGUAGES.map(lang => `` ).join(''); langSelect.onchange = () => { pre.setAttribute('data-language', langSelect.value); }; pre.contentEditable = 'true'; pre.parentNode.insertBefore(wrapper, pre); wrapper.appendChild(langSelect); wrapper.appendChild(pre); } }); } } clear() { this.editableArea.innerHTML = '


'; this.sourceArea.value = ''; } destroy() { this.closePopup(); this.container.innerHTML = ''; } } window.FancyMiku = FancyMiku; })();