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;") )