Skip to content

Commit 6e0ff93

Browse files
daiyippyglove authors
authored andcommitted
Enhancements to HTML controls.
* `lf.views.html.controls.Control`: supports HTML/CSS injection with javascript. * `lf.views.html.controls.Label`: supports dynamic update of CSS classes. * `lf.views.html.controls.TabControl`: - Supports `css_classes` - Allow dynamic addition of new tabs. - Avoid "float: left" for vertical tabs. - Upgraded UI. PiperOrigin-RevId: 704409321
1 parent 0adebc9 commit 6e0ff93

File tree

6 files changed

+226
-88
lines changed

6 files changed

+226
-88
lines changed

pyglove/core/views/html/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def escape(
352352
cls,
353353
s: WritableTypes,
354354
javascript_str: bool = False
355-
) -> Union[str, 'Html', None]:
355+
) -> Any:
356356
"""Escapes an HTML writable object."""
357357
if s is None:
358358
return None

pyglove/core/views/html/controls/base.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def _on_bound(self):
6262
super()._on_bound()
6363
self._rendered = False
6464
self._css_styles = []
65+
self._dynamic_injected_css = set()
6566
self._scripts = []
6667

6768
def add_style(self, *css: str) -> 'HtmlControl':
@@ -76,6 +77,7 @@ def add_script(self, *scripts: str) -> 'HtmlControl':
7677
def to_html(self, **kwargs) -> Html:
7778
"""Returns the HTML representation of the control."""
7879
self._rendered = True
80+
self._dynamic_injected_css = set()
7981
html = self._to_html(**kwargs)
8082
return html.add_style(*self._css_styles).add_script(*self._scripts)
8183

@@ -102,7 +104,7 @@ def _sync_members(self, **fields) -> None:
102104
"""Synchronizes displayed values to members."""
103105
self.rebind(fields, skip_notification=True, raise_on_no_change=False)
104106

105-
def _run_javascript(self, code: str) -> None:
107+
def _run_javascript(self, code: str, debug: bool = False) -> None:
106108
"""Runs the given JavaScript code."""
107109
if not self.interactive:
108110
raise ValueError(
@@ -113,6 +115,8 @@ def _run_javascript(self, code: str) -> None:
113115
return
114116

115117
code = inspect.cleandoc(code)
118+
if debug:
119+
print('RUN JAVSCRIPT:\n', code)
116120
if _notebook is not None:
117121
_notebook.display(_notebook.Javascript(code))
118122

@@ -121,6 +125,40 @@ def _run_javascript(self, code: str) -> None:
121125
for tracked in all_tracked:
122126
tracked.append(code)
123127

128+
def _add_css_rules(self, css: str) -> None:
129+
if not self._rendered or not css or css in self._dynamic_injected_css:
130+
return
131+
self._run_javascript(
132+
f"""
133+
const style = document.createElement('style');
134+
style.type = 'text/css';
135+
style.textContent = "{Html.escape(css, javascript_str=True)}";
136+
document.head.appendChild(style);
137+
"""
138+
)
139+
self._dynamic_injected_css.add(css)
140+
141+
def _apply_css_rules(self, html: Html) -> None:
142+
self._add_css_rules(html.styles.content)
143+
144+
def _insert_adjacent_html(
145+
self,
146+
element_selector_js: str,
147+
html: Html,
148+
var_name: str = 'elem',
149+
position: str = 'beforeend'
150+
):
151+
self._run_javascript(
152+
f"""
153+
{element_selector_js}
154+
{var_name}.insertAdjacentHTML(
155+
"{position}",
156+
"{Html.escape(html, javascript_str=True).to_str(content_only=True)}"
157+
);
158+
""",
159+
)
160+
self._apply_css_rules(html)
161+
124162
def element_id(self, child: Optional[str] = None) -> Optional[str]:
125163
"""Returns the element id of this control or a child."""
126164
if self.id is not None:
@@ -163,14 +201,13 @@ def _update_inner_html(
163201
child: Optional[str] = None,
164202
) -> base.Html:
165203
"""Updates the inner HTML of the control."""
166-
js_html = Html.escape(html, javascript_str=True)
167-
assert isinstance(js_html, Html), js_html
168204
self._run_javascript(
169205
f"""
170206
elem = document.getElementById("{self.element_id(child)}");
171-
elem.innerHTML = "{js_html.to_str(content_only=True)}";
207+
elem.innerHTML = "{Html.escape(html, javascript_str=True).to_str(content_only=True)}";
172208
"""
173209
)
210+
self._add_css_rules(html.styles.content)
174211
return html
175212

176213
def _update_style(

pyglove/core/views/html/controls/label.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def update(
9696
tooltip: Union[str, Html, None] = None,
9797
link: Optional[str] = None,
9898
styles: Optional[Dict[str, Any]] = None,
99+
add_class: Optional[List[str]] = None,
100+
remove_class: Optional[List[str]] = None,
99101
) -> None:
100102
if text is not None:
101103
self._sync_members(text=self._update_content(text))
@@ -105,7 +107,16 @@ def update(
105107
self._sync_members(link=self._update_property('href', link))
106108
if tooltip is not None:
107109
self.tooltip.update(content=tooltip)
108-
110+
if add_class or remove_class:
111+
css_classes = list(self.css_classes)
112+
for x in add_class or []:
113+
self._add_css_class(x)
114+
css_classes.append(x)
115+
for x in remove_class or []:
116+
self._remove_css_class(x)
117+
if x in css_classes:
118+
css_classes.remove(x)
119+
self._sync_members(css_classes=css_classes)
109120

110121
# Register converter for automatic conversion.
111122
pg_typing.register_converter(str, Label, Label)

pyglove/core/views/html/controls/label_test.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,26 @@ def test_with_tooltip(self):
6060
)
6161

6262
def test_update(self):
63-
label = label_lib.Label('foo', 'bar', 'http://google.com', interactive=True)
63+
label = label_lib.Label(
64+
'foo', 'bar', 'http://google.com',
65+
interactive=True,
66+
css_classes=['foo', 'bar'],
67+
)
6468
self.assertIn('id="control-', label.to_html_str(content_only=True))
6569
with label.track_scripts() as scripts:
6670
label.update(
6771
'bar',
6872
tooltip='baz',
6973
link='http://www.yahoo.com',
70-
styles=dict(color='red')
74+
styles=dict(color='red'),
75+
add_class=['baz'],
76+
remove_class=['bar'],
7177
)
7278
self.assertEqual(label.text, 'bar')
7379
self.assertEqual(label.tooltip.content, 'baz')
7480
self.assertEqual(label.link, 'http://www.yahoo.com')
7581
self.assertEqual(label.styles, dict(color='red'))
82+
self.assertEqual(label.css_classes, ['foo', 'baz'])
7683
self.assertEqual(
7784
scripts,
7885
[
@@ -106,6 +113,18 @@ def test_update(self):
106113
elem.classList.remove("html-content");
107114
"""
108115
),
116+
inspect.cleandoc(
117+
f"""
118+
elem = document.getElementById("{label.element_id()}");
119+
elem.classList.add("baz");
120+
"""
121+
),
122+
inspect.cleandoc(
123+
f"""
124+
elem = document.getElementById("{label.element_id()}");
125+
elem.classList.remove("bar");
126+
"""
127+
),
109128
]
110129
)
111130

0 commit comments

Comments
 (0)