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/css/miku/miku.css | 364 +++++++++++++++++ static/css/miku/themes/ghost.css | 102 +++++ static/css/miku/themes/lain.css | 107 +++++ static/css/miku/themes/midnight.css | 138 +++++++ static/css/miku/themes/neoncity.css | 102 +++++ static/css/miku/themes/shifoo.css | 106 +++++ static/css/miku/themes/vintage.css | 106 +++++ static/js/miku/fancymiku.js | 761 ++++++++++++++++++++++++++++++++++++ templates/journals/new_entry.html | 65 ++- 9 files changed, 1848 insertions(+), 3 deletions(-) create mode 100644 static/js/miku/fancymiku.js diff --git a/static/css/miku/miku.css b/static/css/miku/miku.css index 2eb57fbe..15f52718 100644 --- a/static/css/miku/miku.css +++ b/static/css/miku/miku.css @@ -228,4 +228,368 @@ .miku-syntax-js-function { color: #9b59b6; font-weight: 600; +} + +.miku-container { + position: relative; + width: 100%; + border-radius: 6px; + overflow: hidden; + font-family: 'Mali', sans-serif; + background: #0a0a0a; + border: 2px solid #333; +} + +.miku-toolbar { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding: 10px; + background: #1a1a1a; + border-bottom: 1px solid #333; +} + +.miku-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + padding: 6px 10px; + background: #2a2a2a; + border: 1px solid #444; + border-radius: 4px; + color: #e8e8e8; + font-size: 12px; + font-weight: 600; + cursor: pointer; + font-family: 'Mali', sans-serif; +} + +.miku-btn:hover { + background: #3a3a3a; + border-color: #666; +} + +.miku-btn-special { + background: #333; + border-color: #555; +} + +.miku-toolbar-separator { + width: 1px; + height: 24px; + background: #444; + margin: 4px 2px; +} + +.miku-content, +.miku-source { + padding: 16px; + background: #0f0f0f; + color: #e8e8e8; + font-size: 13px; + line-height: 1.6; + overflow-y: auto; + overflow-x: hidden; + min-height: 300px; + outline: none; +} + +.miku-source { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + background: #0a0a0a; + border: none; + resize: none; + width: 100%; + padding: 16px; + color: #e8e8e8; +} + +.miku-content::-webkit-scrollbar, +.miku-source::-webkit-scrollbar { + width: 8px; +} + +.miku-content::-webkit-scrollbar-track, +.miku-source::-webkit-scrollbar-track { + background: #1a1a1a; +} + +.miku-content::-webkit-scrollbar-thumb, +.miku-source::-webkit-scrollbar-thumb { + background: #4ecdc4; + border-radius: 4px; +} + +.miku-content:empty:before, +.miku-content:has(p:only-child:empty):before { + content: attr(data-placeholder); + color: #666; + pointer-events: none; + position: absolute; +} + +.miku-content p { + margin: 4px 0; + text-align: justify; +} + +.miku-content h1, +.miku-content h2, +.miku-content h3 { + margin: 16px 0 8px 0; + color: #4ecdc4; + font-weight: 600; +} + +.miku-content h1 { + font-size: 24px; +} + +.miku-content h2 { + font-size: 20px; +} + +.miku-content h3 { + font-size: 16px; +} + +.miku-content strong, +.miku-content b { + font-weight: 700; + color: #ff6b6b; +} + +.miku-content em, +.miku-content i { + font-style: italic; + color: #f39c12; +} + +.miku-content u { + text-decoration: underline; +} + +.miku-content s { + text-decoration: line-through; + opacity: 0.7; +} + +.miku-content a { + color: #45b7d1; + text-decoration: underline; + cursor: pointer; +} + +.miku-link-edit { + display: inline-block; + margin-left: 4px; + padding: 2px 6px; + background: #333; + color: #4ecdc4; + border-radius: 3px; + cursor: pointer; + font-size: 12px; +} + +.miku-content img { + max-width: 350px; + max-height: 350px; + border-radius: 8px; + border: 1px solid #333; + margin: 6px 12px 8px 0; + cursor: pointer; +} + +.miku-content img:nth-of-type(odd) { + float: left; + margin-right: 12px; +} + +.miku-content img:nth-of-type(even) { + float: right; + margin-left: 12px; +} + +.miku-content img.block { + float: none; + display: block; + margin: 16px auto; + max-width: 100%; + max-height: 500px; + width: auto; +} + +.miku-content a img { + transition: transform 0.2s ease, opacity 0.2s ease; +} + +.miku-content a img:hover { + transform: scale(1.02); + opacity: 0.9; +} + +.miku-content blockquote { + margin: 12px 0; + padding: 12px 16px; + border-left: 4px solid #4ecdc4; + background: rgba(78, 205, 196, 0.1); + border-radius: 0 4px 4px 0; + font-style: italic; +} + +.miku-content code { + background: #1a1a1a; + color: #4ecdc4; + padding: 2px 6px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 12px; + border: 1px solid #333; +} + +.miku-code-wrapper { + position: relative; + margin: 16px 0; + border: 1px solid #333; + border-radius: 6px; + overflow: hidden; +} + +.miku-code-lang { + position: absolute; + top: 8px; + right: 8px; + background: #1a1a1a; + color: #4ecdc4; + padding: 4px 8px; + border: 1px solid #333; + border-radius: 3px; + font-size: 11px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + z-index: 10; + cursor: pointer; +} + +.miku-content pre { + margin: 0; + padding: 40px 12px 12px 12px; + background: #0a0a0a; + overflow-x: auto; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 12px; + line-height: 1.5; + color: #e8e8e8; +} + +.miku-content pre code { + background: transparent; + border: none; + padding: 0; + color: inherit; +} + +.miku-popup { + position: fixed; + width: 400px; + background: #1a1a1a; + border: 2px solid #333; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + z-index: 10000; + font-family: 'Mali', sans-serif; +} + +.miku-popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #2a2a2a; + border-bottom: 1px solid #333; + color: #e8e8e8; + font-weight: 600; +} + +.miku-popup-close { + background: none; + border: none; + color: #e8e8e8; + font-size: 24px; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + line-height: 1; +} + +.miku-popup-body { + padding: 16px; +} + +.miku-popup-field { + margin-bottom: 12px; +} + +.miku-popup-field label { + display: block; + color: #4ecdc4; + margin-bottom: 6px; + font-size: 13px; +} + +.miku-popup-field input[type="text"], +.miku-popup-field select { + width: 100%; + background: #0f0f0f; + border: 1px solid #333; + border-radius: 4px; + padding: 8px; + color: #e8e8e8; + font-family: 'Mali', sans-serif; + font-size: 13px; +} + +.miku-popup-field input[type="checkbox"] { + margin-right: 6px; +} + +.miku-popup-footer { + display: flex; + gap: 8px; + justify-content: flex-end; + padding: 12px 16px; + background: #2a2a2a; + border-top: 1px solid #333; +} + +.miku-btn-primary, +.miku-btn-secondary { + padding: 8px 16px; + border-radius: 4px; + font-weight: 600; + cursor: pointer; + font-family: 'Mali', sans-serif; + font-size: 13px; +} + +.miku-btn-primary { + background: #4ecdc4; + border: 1px solid #4ecdc4; + color: #000; +} + +.miku-btn-primary:hover { + background: #5fd3ca; +} + +.miku-btn-secondary { + background: #2a2a2a; + border: 1px solid #444; + color: #e8e8e8; +} + +.miku-btn-secondary:hover { + background: #3a3a3a; } \ No newline at end of file diff --git a/static/css/miku/themes/ghost.css b/static/css/miku/themes/ghost.css index a57ca488..6a02819b 100644 --- a/static/css/miku/themes/ghost.css +++ b/static/css/miku/themes/ghost.css @@ -145,4 +145,106 @@ .miku-syntax-js-function { color: #00FF00; +} + +.miku-container { + background: #000000; + border: 1px solid #00FFFF; +} + +.miku-toolbar { + background: #000000; + border-bottom: 1px solid #00FFFF; +} + +.miku-btn { + color: #00FFFF; + background: transparent; + border: 1px solid #004444; +} + +.miku-btn:hover { + background: #001111; + border-color: #00FFFF; +} + +.miku-content, +.miku-source { + background: #000000; + color: #00FFFF; +} + +.miku-content::-webkit-scrollbar-thumb, +.miku-source::-webkit-scrollbar-thumb { + background: #00FFFF; +} + +.miku-content h1, +.miku-content h2, +.miku-content h3, +.miku-content a { + color: #00FFFF; +} + +.miku-link-edit { + background: #001111; + color: #00FFFF; +} + +.miku-content img { + border-color: #004444; +} + +.miku-content blockquote { + border-left-color: #00FFFF; + background: #001111; +} + +.miku-content code { + background: #001111; + color: #00FFFF; + border-color: #004444; +} + +.miku-code-wrapper, +.miku-content pre { + border-color: #004444; + background: #001111; +} + +.miku-code-lang { + background: #000000; + color: #00FFFF; + border-color: #00FFFF; +} + +.miku-popup { + background: #000000; + border: 1px solid #00FFFF; +} + +.miku-popup-header, +.miku-popup-footer { + background: #000000; + border-color: #00FFFF; + color: #00FFFF; +} + +.miku-popup-field label, +.miku-popup-field input[type="text"], +.miku-popup-field select { + color: #00FFFF; + background: #001111; + border-color: #00FFFF; +} + +.miku-btn-primary { + background: #00FFFF; + color: #000000; +} + +.miku-btn-secondary { + background: transparent; + color: #00FFFF; + border: 1px solid #00FFFF; } \ No newline at end of file diff --git a/static/css/miku/themes/lain.css b/static/css/miku/themes/lain.css index 3ca95242..eaf816d7 100644 --- a/static/css/miku/themes/lain.css +++ b/static/css/miku/themes/lain.css @@ -145,4 +145,111 @@ .miku-syntax-js-function { color: #AD717B; +} + +.miku-container { + background: #000000; + border: 1px solid #C4748D; +} + +.miku-toolbar { + background: #000000; + border-bottom: 1px solid #C4748D; +} + +.miku-btn { + color: #AD717B; + background: transparent; + border: 1px solid #85474B; +} + +.miku-btn:hover { + background: #340000; + border-color: #C4748D; + color: #D4758C; +} + +.miku-content, +.miku-source { + background: #000000; + color: #AD717B; +} + +.miku-content::-webkit-scrollbar-thumb, +.miku-source::-webkit-scrollbar-thumb { + background: #C4748D; +} + +.miku-content h1, +.miku-content h2, +.miku-content h3, +.miku-content a { + color: #D4758C; +} + +.miku-link-edit { + background: #340000; + color: #D4758C; +} + +.miku-content img { + border-color: #85474B; +} + +.miku-content blockquote { + border-left-color: #C4748D; + background: #340000; + color: #AD717B; +} + +.miku-content code { + background: #340000; + color: #D4758C; + border-color: #85474B; +} + +.miku-code-wrapper, +.miku-content pre { + border-color: #85474B; + background: #340000; +} + +.miku-code-lang { + background: #000000; + color: #AD717B; + border-color: #C4748D; +} + +.miku-popup { + background: #000000; + border: 1px solid #C4748D; +} + +.miku-popup-header, +.miku-popup-footer { + background: #000000; + border-color: #C4748D; + color: #D4758C; +} + +.miku-popup-field label { + color: #AD717B; +} + +.miku-popup-field input[type="text"], +.miku-popup-field select { + background: #340000; + border-color: #C4748D; + color: #AD717B; +} + +.miku-btn-primary { + background: #C4748D; + color: #000000; +} + +.miku-btn-secondary { + background: transparent; + color: #AD717B; + border: 1px solid #C4748D; } \ No newline at end of file diff --git a/static/css/miku/themes/midnight.css b/static/css/miku/themes/midnight.css index 352669b6..c63bdffc 100644 --- a/static/css/miku/themes/midnight.css +++ b/static/css/miku/themes/midnight.css @@ -136,4 +136,142 @@ .miku-syntax-js-function { color: #8d8dff; +} + +.miku-container { + background: #000; + border-color: #8d8dff; +} + +.miku-toolbar { + background: rgba(15, 10, 25, 0.9); + border-bottom-color: rgba(173, 140, 255, 0.4); +} + +.miku-btn { + background: rgba(68, 68, 177, 0.3); + border-color: rgba(141, 141, 255, 0.4); + color: #7ee8c7; +} + +.miku-btn:hover { + background: rgba(223, 35, 196, 0.5); + border-color: #df23c4; + color: #ffffff; +} + +.miku-content, +.miku-source { + background: #000; + color: #fff; +} + +.miku-content::-webkit-scrollbar-thumb, +.miku-source::-webkit-scrollbar-thumb { + background: #df23c4; +} + +.miku-content h1, +.miku-content h2, +.miku-content h3 { + color: #7ee8c7; + text-shadow: 0 0 4px rgba(126, 232, 199, 0.3); +} + +.miku-content strong, +.miku-content b { + color: #96b5ff; +} + +.miku-content em, +.miku-content i { + color: #ff96c4; +} + +.miku-content a { + color: #df23c4; + text-shadow: 0 0 4px rgba(223, 35, 196, 0.3); +} + +.miku-link-edit { + background: rgba(68, 68, 177, 0.8); + color: #7ee8c7; +} + +.miku-content img { + border-color: rgba(141, 141, 255, 0.3); + box-shadow: 0 0 8px rgba(141, 141, 255, 0.2); +} + +.miku-content blockquote { + border-left-color: #8d8dff; + background: rgba(141, 141, 255, 0.1); + color: #96b5ff; +} + +.miku-content code { + background: rgba(0, 0, 0, 0.4); + color: #54f2f2; + border-color: rgba(84, 242, 242, 0.3); +} + +.miku-code-wrapper, +.miku-content pre { + border-color: rgba(141, 141, 255, 0.3); + background: rgba(0, 0, 0, 0.5); +} + +.miku-code-lang { + background: rgba(141, 141, 255, 0.3); + color: #7ee8c7; + border-color: rgba(141, 141, 255, 0.5); +} + +.miku-popup { + background: rgba(30, 20, 50, 0.95); + border-color: rgba(173, 140, 255, 0.7); + box-shadow: 0 8px 24px rgba(136, 87, 210, 0.6); +} + +.miku-popup-header { + background: rgba(50, 30, 80, 0.95); + border-bottom-color: rgba(173, 140, 255, 0.5); +} + +.miku-popup-field label { + color: #7ee8c7; +} + +.miku-popup-field input[type="text"], +.miku-popup-field select { + background: rgba(0, 0, 0, 0.6); + border-color: rgba(141, 141, 255, 0.4); + color: #fff; +} + +.miku-popup-footer { + background: rgba(50, 30, 80, 0.95); + border-top-color: rgba(173, 140, 255, 0.5); +} + +.miku-btn-primary { + background: rgba(68, 68, 177, 0.85); + border-color: #8d8dff; + color: #fff; +} + +.miku-btn-primary:hover { + background: #df23c4; + border-color: #df23c4; +} + +.miku-btn-secondary { + background: rgba(68, 68, 177, 0.3); + border-color: rgba(141, 141, 255, 0.4); + color: #96b5ff; +} + +.miku-btn-secondary:hover { + background: rgba(150, 181, 255, 0.2); + color: #ffffff; } \ No newline at end of file diff --git a/static/css/miku/themes/neoncity.css b/static/css/miku/themes/neoncity.css index 963207f3..7ed96c89 100644 --- a/static/css/miku/themes/neoncity.css +++ b/static/css/miku/themes/neoncity.css @@ -146,4 +146,106 @@ .miku-syntax-js-function { color: #00ff00; font-weight: bold; +} + +.miku-container { + background: #000000; + border: none; +} + +.miku-toolbar { + background: #000000; + border-bottom: 1px solid #00ff00; +} + +.miku-btn { + color: #00ff00; + background: transparent; + border: 1px solid #003300; +} + +.miku-btn:hover { + background: #111111; + border-color: #00ff00; +} + +.miku-content, +.miku-source { + background: #000000; + color: #00ff00; +} + +.miku-content::-webkit-scrollbar-thumb, +.miku-source::-webkit-scrollbar-thumb { + background: #00ff00; +} + +.miku-content h1, +.miku-content h2, +.miku-content h3, +.miku-content a { + color: #00ff00; +} + +.miku-link-edit { + background: #111111; + color: #00ff00; +} + +.miku-content img { + border-color: #003300; +} + +.miku-content blockquote { + border-left-color: #00ff00; + background: #111111; +} + +.miku-content code { + background: #111111; + color: #00ff00; + border-color: #003300; +} + +.miku-code-wrapper, +.miku-content pre { + border-color: #003300; + background: #111111; +} + +.miku-code-lang { + background: #000000; + color: #00ff00; + border-color: #00ff00; +} + +.miku-popup { + background: #000000; + border: 1px solid #00ff00; +} + +.miku-popup-header, +.miku-popup-footer { + background: #000000; + border-color: #00ff00; + color: #00ff00; +} + +.miku-popup-field label, +.miku-popup-field input[type="text"], +.miku-popup-field select { + color: #00ff00; + background: #111111; + border-color: #00ff00; +} + +.miku-btn-primary { + background: #00ff00; + color: #000000; +} + +.miku-btn-secondary { + background: transparent; + color: #00ff00; + border: 1px solid #00ff00; } \ No newline at end of file diff --git a/static/css/miku/themes/shifoo.css b/static/css/miku/themes/shifoo.css index 4a3abe28..8da6c0eb 100644 --- a/static/css/miku/themes/shifoo.css +++ b/static/css/miku/themes/shifoo.css @@ -142,4 +142,110 @@ .miku-syntax-js-function { color: #5ccfe6; +} + +.miku-container { + background: #0a0e14; + border: none; +} + +.miku-toolbar { + background: #0a0e14; + border-bottom: 1px solid #4d5566; +} + +.miku-btn { + color: #b3b1ad; + background: transparent; + border: 1px solid #1a1f29; +} + +.miku-btn:hover { + background: #1a1f29; + border-color: #36a3d9; + color: #ffb454; +} + +.miku-content, +.miku-source { + background: #0a0e14; + color: #b3b1ad; +} + +.miku-content::-webkit-scrollbar-thumb, +.miku-source::-webkit-scrollbar-thumb { + background: #36a3d9; +} + +.miku-content h1, +.miku-content h2, +.miku-content h3, +.miku-content a { + color: #36a3d9; +} + +.miku-link-edit { + background: #1a1f29; + color: #ffb454; +} + +.miku-content img { + border-color: #4d5566; +} + +.miku-content blockquote { + border-left-color: #36a3d9; + background: #1a1f29; +} + +.miku-content code { + background: #1a1f29; + color: #ffb454; + border-color: #4d5566; +} + +.miku-code-wrapper, +.miku-content pre { + border-color: #4d5566; + background: #1a1f29; +} + +.miku-code-lang { + background: #0a0e14; + color: #b3b1ad; + border-color: #36a3d9; +} + +.miku-popup { + background: #0a0e14; + border: 1px solid #4d5566; +} + +.miku-popup-header, +.miku-popup-footer { + background: #0a0e14; + border-color: #4d5566; + color: #ffb454; +} + +.miku-popup-field label { + color: #b3b1ad; +} + +.miku-popup-field input[type="text"], +.miku-popup-field select { + background: #1a1f29; + border-color: #4d5566; + color: #b3b1ad; +} + +.miku-btn-primary { + background: #36a3d9; + color: #0a0e14; +} + +.miku-btn-secondary { + background: transparent; + color: #b3b1ad; + border: 1px solid #4d5566; } \ No newline at end of file diff --git a/static/css/miku/themes/vintage.css b/static/css/miku/themes/vintage.css index 34afcee8..65b4ec19 100644 --- a/static/css/miku/themes/vintage.css +++ b/static/css/miku/themes/vintage.css @@ -139,4 +139,110 @@ .miku-syntax-js-function { color: #8d8dff; +} + +.miku-container { + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.miku-toolbar { + background: rgba(0, 0, 0, 0.2); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.miku-btn { + color: #ffffff; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.miku-btn:hover { + background: rgba(141, 141, 255, 0.2); + border-color: #8d8dff; + color: #8d8dff; +} + +.miku-content, +.miku-source { + background: transparent; + color: #ffffff; +} + +.miku-content::-webkit-scrollbar-thumb, +.miku-source::-webkit-scrollbar-thumb { + background: #8d8dff; +} + +.miku-content h1, +.miku-content h2, +.miku-content h3, +.miku-content a { + color: #8d8dff; +} + +.miku-link-edit { + background: rgba(0, 0, 0, 0.4); + color: #8d8dff; +} + +.miku-content img { + border-color: rgba(255, 255, 255, 0.1); +} + +.miku-content blockquote { + border-left-color: #8d8dff; + background: rgba(0, 0, 0, 0.3); +} + +.miku-content code { + background: rgba(0, 0, 0, 0.4); + color: #8d8dff; + border-color: rgba(255, 255, 255, 0.1); +} + +.miku-code-wrapper, +.miku-content pre { + border-color: rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.4); +} + +.miku-code-lang { + background: rgba(0, 0, 0, 0.3); + color: #ffffff; + border-color: rgba(141, 141, 255, 0.3); +} + +.miku-popup { + background: rgba(0, 0, 0, 0.95); + border: 1px solid rgba(141, 141, 255, 0.3); +} + +.miku-popup-header, +.miku-popup-footer { + background: rgba(0, 0, 0, 0.95); + border-color: rgba(255, 255, 255, 0.1); + color: #8d8dff; +} + +.miku-popup-field label { + color: #ffffff; +} + +.miku-popup-field input[type="text"], +.miku-popup-field select { + background: rgba(0, 0, 0, 0.5); + border-color: rgba(141, 141, 255, 0.3); + color: #ffffff; +} + +.miku-btn-primary { + background: #8d8dff; + color: #000000; +} + +.miku-btn-secondary { + background: transparent; + color: #ffffff; + border: 1px solid rgba(255, 255, 255, 0.3); } \ No newline at end of file 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; +})(); diff --git a/templates/journals/new_entry.html b/templates/journals/new_entry.html index 15e7ad32..aae8e41f 100644 --- a/templates/journals/new_entry.html +++ b/templates/journals/new_entry.html @@ -3,11 +3,70 @@ {% load i18n %} {% block head %} - + + + {% endblock head %} {% block content %} -
+
{% include "_partials/journal_header.html" with journal=journal %} -
+ +
+
+ {% csrf_token %} + +
+
+ + +
+ +
+ +
+ +
+
+ +
+ + +
+
+
+
{% endblock content %} + +{% block scripts %} + + +{% endblock scripts %} -- cgit v1.2.3