(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 = `
`;
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 = `
`;
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;
})();