149 lines
6.5 KiB
Python
149 lines
6.5 KiB
Python
from nicegui import ui
|
|
|
|
|
|
def rich_text_editor() -> ui.editor:
|
|
"""A rich text editor component with a customizable toolbar and styling."""
|
|
toolbar = (
|
|
"["
|
|
+ "['bold', 'italic', 'strike', 'underline'],"
|
|
+ "['quote', 'unordered', 'ordered', 'code'],"
|
|
+ "['link', 'remove-formatting'],"
|
|
+ "['print', 'fullscreen'],"
|
|
+ "['viewsource']"
|
|
+ "]"
|
|
)
|
|
|
|
props_string = (
|
|
f"toolbar={toolbar} "
|
|
+ "content-style='font-size: 16px; line-height: 1.6; color: white;' "
|
|
+ "toolbar-bg='gray-800' "
|
|
+ "toolbar-text-color='white' "
|
|
+ "toolbar-toggle-color='yellow-8'"
|
|
)
|
|
editor = (
|
|
ui.editor(placeholder="Start writing or use Create Template...")
|
|
.classes("flex-grow bg-gray-800 text-white")
|
|
.props(props_string)
|
|
)
|
|
|
|
def attach_paste_handler():
|
|
# This script runs after the editor's Vue component is mounted,
|
|
# ensuring the element is available in the DOM.
|
|
# It uses a raw f-string (rf"...") to handle backslashes in the regex
|
|
# and doubled curly braces ({{...}}) for JavaScript code blocks.
|
|
_ = ui.run_javascript(rf"""
|
|
const editorElement = document.getElementById('{editor.id}');
|
|
if (editorElement) {{
|
|
const contentArea = editorElement.querySelector('.q-editor__content');
|
|
if (contentArea) {{
|
|
// Function to parse a CSS color string into an [r, g, b] array.
|
|
const parseColor = (colorStr) => {{
|
|
const d = document.createElement("div");
|
|
d.style.color = colorStr;
|
|
document.body.appendChild(d);
|
|
const computedColor = window.getComputedStyle(d).color;
|
|
document.body.removeChild(d);
|
|
const parts = computedColor.match(/rgba?\((\d+), (\d+), (\d+)\)/);
|
|
if (parts) {{
|
|
return [parseInt(parts[1]), parseInt(parts[2]), parseInt(parts[3])];
|
|
}}
|
|
return null;
|
|
}};
|
|
|
|
// Function to calculate the perceived luminance of a color.
|
|
const getLuminance = (r, g, b) => {{
|
|
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
}};
|
|
|
|
const paste_handler = (evt) => {{
|
|
evt.preventDefault();
|
|
const clipboardData = evt.clipboardData;
|
|
if (!clipboardData) {{ return; }}
|
|
|
|
let pastedHtml = clipboardData.getData('text/html');
|
|
const selection = window.getSelection();
|
|
if (!selection || !selection.rangeCount) {{ return; }}
|
|
selection.deleteFromDocument();
|
|
const range = selection.getRangeAt(0);
|
|
|
|
if (pastedHtml) {{
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = pastedHtml;
|
|
|
|
tempDiv.querySelectorAll('*').forEach(el => {{
|
|
el.style.backgroundColor = '';
|
|
el.style.background = '';
|
|
|
|
let colorToCheck = el.style.color;
|
|
if (el.tagName === 'FONT' && el.getAttribute('color')) {{
|
|
colorToCheck = el.getAttribute('color');
|
|
}}
|
|
|
|
if (colorToCheck) {{
|
|
const rgb = parseColor(colorToCheck);
|
|
if (rgb) {{
|
|
const luminance = getLuminance(rgb[0], rgb[1], rgb[2]);
|
|
// A luminance threshold of 100 is a good starting point
|
|
// for identifying dark colors on a dark background.
|
|
if (luminance < 100) {{
|
|
el.style.color = ''; // Reset to inherit default color
|
|
if (el.tagName === 'FONT') {{
|
|
el.removeAttribute('color');
|
|
}}
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
if (el.hasAttribute('style') && !el.getAttribute('style').trim()) {{
|
|
el.removeAttribute('style');
|
|
}}
|
|
}});
|
|
|
|
const sanitizedHtml = tempDiv.innerHTML;
|
|
const fragment = range.createContextualFragment(sanitizedHtml);
|
|
const lastNode = fragment.lastChild;
|
|
range.insertNode(fragment);
|
|
|
|
if (lastNode) {{
|
|
const newRange = document.createRange();
|
|
newRange.setStartAfter(lastNode);
|
|
newRange.collapse(true);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newRange);
|
|
}}
|
|
}} else {{
|
|
const pastedText = clipboardData.getData('text/plain');
|
|
if (pastedText) {{
|
|
const textNode = document.createTextNode(pastedText);
|
|
range.insertNode(textNode);
|
|
const newRange = document.createRange();
|
|
newRange.setStartAfter(textNode);
|
|
newRange.collapse(true);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newRange);
|
|
}}
|
|
}}
|
|
}};
|
|
contentArea.addEventListener('paste', paste_handler);
|
|
}} else {{
|
|
console.error('Error: Could not find the .q-editor__content element.');
|
|
}}
|
|
}} else {{
|
|
console.error(`Error: Could not find editor element with ID: {editor.id}`);
|
|
}}
|
|
""")
|
|
|
|
_ = editor.on("vue-mounted", attach_paste_handler)
|
|
|
|
return editor
|
|
|
|
|
|
def markdown_editor() -> ui.textarea:
|
|
"""A markdown-native editor with stable scrolling and no HTML conversion."""
|
|
return (
|
|
ui.textarea(placeholder="Write markdown here...")
|
|
.props("outlined autogrow=false")
|
|
.classes("bg-gray-800 text-white journal-markdown-editor")
|
|
.style("flex: 1; width: 100%; min-height: 0;")
|
|
)
|