From e3287c2bb8bbcace5ecabc7ce2bf2ed06897e906 Mon Sep 17 00:00:00 2001 From: Bobby <30593201+luciferreeves@users.noreply.github.com> Date: Sun, 8 Feb 2026 05:47:43 +0000 Subject: Added Fancy version of Miku with styles and new entry page for journals --- static/js/miku/fancymiku.js | 761 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 761 insertions(+) create mode 100644 static/js/miku/fancymiku.js (limited to 'static/js') diff --git a/static/js/miku/fancymiku.js b/static/js/miku/fancymiku.js new file mode 100644 index 00000000..adfd2197 --- /dev/null +++ b/static/js/miku/fancymiku.js @@ -0,0 +1,761 @@ +(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 buttons = [ + { icon: 'H1', title: 'Heading 1', command: 'heading', value: 'h1' }, + { icon: 'H2', title: 'Heading 2', command: 'heading', value: 'h2' }, + { icon: 'H3', title: 'Heading 3', command: 'heading', value: 'h3' }, + { separator: true }, + { icon: 'B', title: 'Bold', command: 'bold' }, + { icon: 'I', title: 'Italic', command: 'italic' }, + { icon: 'U', title: 'Underline', command: 'underline' }, + { icon: 'S', title: 'Strikethrough', command: 'strikethrough' }, + { separator: true }, + { icon: '🔗', title: 'Insert Link', command: 'link' }, + { icon: '🖼️', title: 'Insert Image', command: 'image' }, + { separator: true }, + { icon: '❝❞', title: 'Blockquote', command: 'blockquote' }, + { icon: '', title: 'Code Block', command: 'codeblock' }, + { icon: '`', title: 'Inline Code', command: 'inlinecode' }, + { separator: true }, + { icon: '⇄', 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 'blockquote': + this.insertBlockquote(); + break; + case 'codeblock': + this.insertCodeBlock(); + return; + case 'inlinecode': + this.insertInlineCode(); + break; + } + + this.saveSelection(); + } + + toggleSource() { + if (this.isSourceMode) { + this.setContent(this.sourceArea.value); + this.editableArea.style.display = 'block'; + this.sourceArea.style.display = 'none'; + this.isSourceMode = false; + } else { + this.sourceArea.value = this.getContent(); + this.editableArea.style.display = 'none'; + this.sourceArea.style.display = 'block'; + this.isSourceMode = true; + } + } + + 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 === '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(); + } + + 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 lines = text.split('\n'); + lines.forEach((line, index) => { + const p = document.createElement('p'); + p.textContent = line || '\u00A0'; + range.insertNode(p); + + if (index < lines.length - 1) { + range.setStartAfter(p); + range.setEndAfter(p); + } + }); + } + + 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; +})(); -- cgit v1.2.3