from __future__ import annotations
from typing import Any
from ..base import Widget
[docs]
class Text(Widget):
"""A multi-line text input area."""
def __init__(
self,
parent: Widget | None = None,
text: str = "",
**kwargs: Any,
) -> None:
super().__init__(parent, **kwargs)
self._config_dict["text"] = (
self._textvariable.get() if self._textvariable is not None else str(text)
)
@property
def text(self) -> str:
return self._config_dict.get("text", "")
@text.setter
def text(self, val: str) -> None:
self._config_dict["text"] = str(val)
self._sync()
[docs]
def append(self, text: str) -> None:
self._config_dict["text"] += str(text)
self._sync()
[docs]
def clear(self) -> None:
self._config_dict["text"] = ""
self._sync()
def _var_updated(self, var: Any) -> None:
if var is self._textvariable:
self._config_dict["text"] = var.get()
self._sync()
def _handle_bridge_event(self, event_name: str, event_data: Any) -> None:
if event_name == "change":
self._config_dict["text"] = str(event_data)
super()._handle_bridge_event(event_name, event_data)
def _render(self) -> str:
val = self._config_dict.get("text", "")
escaped = val.replace("&", "&").replace("<", "<").replace(">", ">")
style = self._render_style()
width = self._config_dict.get("width", 200)
height = self._config_dict.get("height", 100)
attrs = self._render_attrs()
return f'<textarea id="{self._id}" class="iskg-text" style="{style}width:{width}px;height:{height}px;" {attrs}>{escaped}</textarea>'
def _render_js(self) -> str:
return f'''document.getElementById("{self._id}").onchange=function(){{
iskg_bridge_event("{self._id}","change",this.value);
}};'''
def _default_takefocus(self) -> bool:
return True
def _render_update_js(self) -> str:
val = self._config_dict.get("text", "")
escaped = val.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n")
return f'iskg_set_text("{self._id}","{escaped}");'
[docs]
class RichText(Widget):
"""A rich text editor with formatting toolbar."""
def __init__(
self,
parent: Widget | None = None,
text: str = "",
height: int = 150,
show_toolbar: bool = True,
**kwargs: Any,
) -> None:
super().__init__(parent, **kwargs)
self._config_dict["text"] = text
self._config_dict["height"] = height
self._config_dict["show_toolbar"] = show_toolbar
def _render(self) -> str:
text = self._config_dict.get("text", "")
height = self._config_dict.get("height", 150)
show_tb = self._config_dict.get("show_toolbar", True)
style = self._render_style()
tb_html = ""
if show_tb:
cmds: list[tuple[str, str, str]] = [
("B", "bold", "Bold"),
("I", "italic", "Italic"),
("U", "underline", "Underline"),
("S", "strikeThrough", "Strikethrough"),
("", "", ""),
("\u25cf", "forecolor:#ef4444", "Red"),
("\u25cf", "forecolor:#4ade80", "Green"),
("\u25cf", "forecolor:#22d3ee", "Cyan"),
("\u25cf", "forecolor:#f59e0b", "Amber"),
("", "", ""),
("H1", "formatBlock:h1", "Heading 1"),
("H2", "formatBlock:h2", "Heading 2"),
("P", "formatBlock:p", "Paragraph"),
("", "", ""),
("\u2261", "insertUnorderedList", "Bullet list"),
("1.", "insertOrderedList", "Numbered list"),
]
btns = ""
for label, cmd, tip in cmds:
if cmd == "":
btns += '<span class="iskg-rt-sep"></span>'
else:
args = ""
c = cmd
if ":" in cmd:
c, args = cmd.split(":", 1)
color_style = ""
if c == "forecolor":
color_style = f" color:{args};"
btns += (
f'<button data-cmd="{c}" data-args="{args}"'
f' title="{tip}" style="{color_style}">{label}</button>'
)
tb_html = f'<div class="iskg-richtext-toolbar">{btns}</div>'
return f'''<div id="{self._id}" class="iskg-richtext-wrap" style="{style}height:{height}px;">
{tb_html}
<div id="{self._id}-editor" class="iskg-richtext-editor" contenteditable="true">{text}</div>
</div>'''
def _render_js(self) -> str:
return f'''(function(){{
var wrap=document.getElementById("{self._id}");
if(!wrap)return;
var editor=wrap.querySelector(".iskg-richtext-editor");
var toggleCmds={{"bold":true,"italic":true,"underline":true,"strikeThrough":true}};
function updateActiveStates(){{
wrap.querySelectorAll(".iskg-richtext-toolbar button").forEach(function(b){{
var c=b.dataset.cmd;
if(toggleCmds[c]){{
try{{b.classList.toggle("active",document.queryCommandState(c));}}catch(e){{}}
}}
}});
}}
editor.addEventListener("mouseup",updateActiveStates);
editor.addEventListener("keyup",updateActiveStates);
wrap.querySelectorAll(".iskg-richtext-toolbar button").forEach(function(btn){{
btn.onmousedown=function(e){{e.preventDefault();editor.focus();}};
btn.onclick=function(){{
var cmd=this.dataset.cmd;
var args=this.dataset.args;
if(cmd=="formatBlock"){{
document.execCommand("formatBlock",false,args||"<p>");
}}else if(cmd=="forecolor"){{
document.execCommand("foreColor",false,args||"#22d3ee");
}}else{{
document.execCommand(cmd,false,args||null);
}}
editor.focus();
updateActiveStates();
}};
}});
editor.oninput=function(){{
iskg_bridge_event("{self._id}","change",this.innerHTML);
}};
updateActiveStates();
}})();'''
def _render_update_js(self) -> str:
text = self._config_dict.get("text", "")
escaped = (
text.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("/", "\\/")
)
return f'''var el=document.getElementById("{self._id}-editor");
if(el)el.innerHTML="{escaped}";'''