ipyslides 7.3.2__tar.gz → 7.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {ipyslides-7.3.2 → ipyslides-7.3.4}/PKG-INFO +1 -1
  2. ipyslides-7.3.4/ipyslides/__version__.py +2 -0
  3. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/_layout.py +8 -2
  4. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/_widgets.py +1 -1
  5. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/export_html.py +2 -3
  6. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/export_template.py +15 -9
  7. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/intro.py +2 -2
  8. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/navigation.py +5 -5
  9. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/settings.py +35 -25
  10. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/slide.py +15 -30
  11. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/styles.py +34 -51
  12. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/widgets.py +7 -11
  13. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/core.py +11 -18
  14. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/utils.py +14 -1
  15. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/xmd.py +1 -1
  16. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides.egg-info/PKG-INFO +1 -1
  17. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides.egg-info/SOURCES.txt +1 -8
  18. ipyslides-7.3.2/ipyslides/__version__.py +0 -2
  19. ipyslides-7.3.2/ipyslides/_base/static/interaction.css +0 -469
  20. ipyslides-7.3.2/ipyslides/_base/static/interaction.js +0 -1013
  21. ipyslides-7.3.2/ipyslides/_base/static/laser.css +0 -31
  22. ipyslides-7.3.2/ipyslides/_base/static/laser.js +0 -100
  23. ipyslides-7.3.2/ipyslides/_base/static/notes.js +0 -122
  24. ipyslides-7.3.2/ipyslides/pkg_nbs/ips-demo.ipynb +0 -630
  25. ipyslides-7.3.2/ipyslides/pkg_nbs/ips-docs.ipynb +0 -582
  26. {ipyslides-7.3.2 → ipyslides-7.3.4}/LICENSE +0 -0
  27. {ipyslides-7.3.2 → ipyslides-7.3.4}/README.md +0 -0
  28. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/__init__.py +0 -0
  29. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/__init__.py +0 -0
  30. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/_syntax.py +0 -0
  31. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/base.py +0 -0
  32. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/icons.py +0 -0
  33. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/_base/notes.py +0 -0
  34. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/dashlab.py +0 -0
  35. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/formatters.py +0 -0
  36. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/source.py +0 -0
  37. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides/writer.py +0 -0
  38. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides.egg-info/dependency_links.txt +0 -0
  39. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides.egg-info/requires.txt +0 -0
  40. {ipyslides-7.3.2 → ipyslides-7.3.4}/ipyslides.egg-info/top_level.txt +0 -0
  41. {ipyslides-7.3.2 → ipyslides-7.3.4}/setup.cfg +0 -0
  42. {ipyslides-7.3.2 → ipyslides-7.3.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ipyslides
3
- Version: 7.3.2
3
+ Version: 7.3.4
4
4
  Summary: Live rich content slides in jupyter notebook
5
5
  Home-page: https://github.com/asaboor-gh/ipyslides
6
6
  Author: Abdul Saboor
@@ -0,0 +1,2 @@
1
+
2
+ __version__ = "7.3.4"
@@ -48,7 +48,9 @@ def layout_css(accent_color, aspect):
48
48
  },
49
49
  },
50
50
  ".static-widget": {
51
- "position": "static !important", # let objects flow out
51
+ "^, > *, .widget-html-content": {
52
+ "position": "static !important", # let objects flow out
53
+ },
52
54
  },
53
55
  ".Warn::after": {
54
56
  "display": "block",
@@ -131,6 +133,11 @@ def layout_css(accent_color, aspect):
131
133
  "position": "absolute !important",
132
134
  "^, .widget-html-content": {"line-height": "0 !important"},
133
135
  "@media print": {"position": "fixed !important",},
136
+ "@media screen": { # avoid messing up print layout
137
+ "transform": "scale(min(var(--contentScale), 1)) !important", # scale footer to match content, but not more than 1
138
+ "transform-origin": "left bottom !important",
139
+ "transition": "transform 0.4s ease-in-out",
140
+ },
134
141
  }, # other properties are set internally
135
142
  "> .Build-Btn": {"visibility": "hidden !important",}, # hide build button by default
136
143
  "^:has(.SlideArea.Stale) > .Build-Btn": {
@@ -328,7 +335,6 @@ def layout_css(accent_color, aspect):
328
335
  "line-height": "1.5 !important",
329
336
  },
330
337
  "^, .widget-html-content": {
331
- "--text-size": "14px !important",
332
338
  "position": "static !important", # footer content should escape
333
339
  },
334
340
  },
@@ -23,7 +23,7 @@ class InteractionWidget(anywidget.AnyWidget):
23
23
  _colors = traitlets.Dict(jupyter_colors).tag(sync=True) # for export
24
24
  _parts = traitlets.Dict().tag(sync=True) # parts data for each slide, for js side use
25
25
  _main_end = traitlets.Int(default_value=0).tag(sync=True) # last main slide index
26
- _fpad = traitlets.Int(16).tag(sync=True) # padding for footer, used in export
26
+ _page_css = traitlets.Unicode('').tag(sync=True) # page size and margin for print/export
27
27
 
28
28
  msg_topy = traitlets.Unicode('').tag(sync=True)
29
29
  msg_tojs = traitlets.Unicode('').tag(sync=True)
@@ -28,7 +28,6 @@ _script = '''<script>
28
28
  let scale = scaleH > scaleW ? scaleW : scaleH;
29
29
  if (scale) { // Only add if not null or somethings
30
30
  document.documentElement.style.setProperty('--contentScale',scale);
31
- document.documentElement.style.setProperty('--paddingBottom',Number(__PADBTM__/scale) + "px");
32
31
  };
33
32
  };
34
33
  window.dispatchEvent(new window.Event('resize')); // First time programatically
@@ -111,7 +110,7 @@ class _HhtmlExporter:
111
110
  theme_kws["colors"] = self.main.widgets.iw._colors
112
111
 
113
112
  dom_classes = tuple(self.main._box._dom_classes)
114
- css_classes = [c for c in dom_classes if c.startswith('Slides')]
113
+ css_classes = [c for c in dom_classes if c.startswith('Slides') or c.startswith('has-')] # only export relevant classes
115
114
  if self.main.uid in dom_classes:
116
115
  css_classes.append(self.main.uid)
117
116
 
@@ -122,7 +121,7 @@ class _HhtmlExporter:
122
121
  content = content,
123
122
  script = _script,
124
123
  css_class = ' '.join(css_classes),
125
- padding_bottom = self.main.widgets.iw._fpad,
124
+ page_css = self.main.html('style', self.main.widgets.iw._page_css).value,
126
125
  )
127
126
 
128
127
 
@@ -1,6 +1,6 @@
1
1
  # Template for building HTML from slides
2
2
 
3
- def doc_html(code_css, style_css, content, script, css_class, padding_bottom):
3
+ def doc_html(code_css, style_css, content, script, css_class, page_css = ''):
4
4
  return f'''<!DOCTYPE html>
5
5
  <html lang="en">
6
6
  <head>
@@ -13,8 +13,9 @@ def doc_html(code_css, style_css, content, script, css_class, padding_bottom):
13
13
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
14
14
 
15
15
  {style_css}
16
- {slides_css.replace("__PADBTM__",str(padding_bottom))}
16
+ {slides_css}
17
17
  {code_css}
18
+ {page_css}
18
19
 
19
20
  <!-- Custom stylesheet, it must be in the same directory as the html file -->
20
21
  <link rel="stylesheet" href="overrides.css">
@@ -50,7 +51,7 @@ def doc_html(code_css, style_css, content, script, css_class, padding_bottom):
50
51
  </div>
51
52
  </div>
52
53
  </body>
53
- {script.replace("__PADBTM__",str(padding_bottom))}
54
+ {script}
54
55
  </html>
55
56
  '''
56
57
 
@@ -94,6 +95,9 @@ section .SlideBox {
94
95
  .SlidesWrapper .SlideLogo {
95
96
  position: fixed !important;
96
97
  line-height: 0 !important; /* suppress bad line-height here */
98
+ transform: scale(min(var(--contentScale), 1)) !important; /* scale footer to match content, but not more than 1 */
99
+ transform-origin: left bottom !important;
100
+ transition: transform 0.4s ease-in-out;
97
101
  }
98
102
  .SlidesWrapper::-webkit-scrollbar,
99
103
  .SlidesWrapper::-webkit-scrollbar-button,
@@ -103,7 +107,7 @@ section .SlideBox {
103
107
 
104
108
  .SlidesWrapper.Scrolling .slide-footer {
105
109
  visibility: hidden !important;
106
- transition: visbility 200ms ease-in;
110
+ transition: visibility 200ms ease-in;
107
111
  }
108
112
 
109
113
  @media print {
@@ -137,12 +141,15 @@ section .SlideBox {
137
141
  margin: 0 !important;
138
142
  padding: 0 !important;
139
143
  page-break-inside: avoid !important;
140
- page-break-after: always !important;
144
+ page-break-before: always !important;
145
+ break-before: page !important;
146
+ break-inside: avoid !important;
141
147
  overflow: hidden !important; /* otherwise it throws text to next page */
142
- padding-bottom: __PADBTM__px !important; /* for avoiding overflow to bottom,same as under SlideArea */
148
+ padding-bottom: var(--pad-footer, 23px) !important; /* for avoiding overflow to bottom,same as under SlideArea */
143
149
  }
144
- section:last-of-type {
145
- page-break-after: avoid !important;
150
+ section:first-of-type {
151
+ page-break-before: avoid !important;
152
+ break-before: avoid !important;
146
153
  }
147
154
  section .SlideBox {
148
155
  margin-bottom: 0 !important;
@@ -153,7 +160,6 @@ section .SlideBox {
153
160
  position: static !important; /*override from document as printing absolute is issue */
154
161
  overflow: hidden !important;
155
162
  page-break-inside: avoid !important;
156
- --paddingBottom: __PADBTM__px; /* fixed for printing, padding set in style_css */
157
163
  }
158
164
  }
159
165
  </style>
@@ -123,7 +123,7 @@ Use `Save as PDF` option instead of Print PDF in browser to make links work in o
123
123
  - PDF printing may not work as expected in all browsers/IDEs and tested only in JupyterLab.
124
124
  - In case of issues with frames not displaying properly using `Ctrl + P`, export to HTML and then print.
125
125
  - Make sure your presentation is clean for print. "Inline Notes" are only meant for personal reference or sharing slides with notes.
126
- - `Slides.settings.layout.aspect` may be adjusted for a little different than standard aspect ratios to avoid rounding issues in print, such as 16/9 to 16/9.001, etc.
126
+ - If print shows blank/gray pages, adjust printer setting settings such as margin and it would resolve the issue. If not, export to HTML and then print.
127
127
 
128
128
  **Printing from Exported HTML File**<br>
129
129
  You can also get PDF from exported HTML file. Use `Save as PDF` and enable background graphics when printing to keep links working.
@@ -133,7 +133,7 @@ slides, use [alert! slides.alt /]. You can paste screenshots from system tool
133
133
  into Clips GUI in side panel. On Linux, you need [alert! xclip /] or [alert! wl-paste /] installed.
134
134
 
135
135
  ::: note-tip
136
- - You might want to reflow content for export purpose. Use checkbox in settings panel to enable it.
136
+ - You might want to flow content for export purpose. Use checkbox in settings panel to enable it.
137
137
  - Print slides with notes by enabling `Inline Notes` option in settings panel before print/export. Notes appear at top to grab immediate attetion of the speaker.
138
138
  - Use `Merge Parts` option in settings panel to show all parts of a page/slide as one page/slide.
139
139
  """
@@ -3,6 +3,8 @@ Author Notes: Classes in this module should only be instantiated in Slides class
3
3
  and then provided to other classes via composition, not inheritance.
4
4
  """
5
5
 
6
+ from ..utils import update_class
7
+
6
8
  class Navigation:
7
9
  def __init__(self, _instanceSlides):
8
10
  "Instnace should be inside `Slides` class."
@@ -16,17 +18,15 @@ class Navigation:
16
18
  self.btn_next.on_click(self._shift_right)
17
19
 
18
20
  def _shift_right(self,change):
19
- self.widgets.slidebox.remove_class('Prev') # remove backwards animation safely
20
- self.widgets.slidebox.remove_class('AnimPrev') # content animation flag
21
+ update_class(self.widgets.slidebox, 'Prev AnimPrev', False) # remove backwards animation safely
21
22
  if change and not self.slides._current.next_frame():
22
23
  if self.wprogress.value < self.wprogress.max:
23
24
  self.wprogress.value = self.wprogress.value + 1 # Forwards
24
25
 
25
26
  def _shift_left(self,change):
26
- self.widgets.slidebox.remove_class('Prev') # remove backwards animation safely, need trigger before adding
27
+ self.widgets.slidebox.remove_class('Prev') # remove backwards animation safely, need trigger before adding, but avoid resetting AnimPrev here
27
28
  if change and not self.slides._current.prev_frame():
28
- self.widgets.slidebox.add_class('Prev') # Backwards Animation
29
- self.widgets.slidebox.add_class('AnimPrev') # content animation flag, need to stay persistent to avoid trigger removal too early
29
+ update_class(self.widgets.slidebox, 'Prev AnimPrev', True)
30
30
  if self.wprogress.value > 0:
31
31
  self.wprogress.value = self.wprogress.value - 1 # Backwards
32
32
 
@@ -4,6 +4,7 @@ and then provided to other classes via composition, not inheritance.
4
4
  """
5
5
  import json, re
6
6
  import traitlets
7
+ import textwrap
7
8
 
8
9
  from traitlets import HasTraits, Int, Unicode, Bool, Float, TraitError
9
10
  from pathlib import Path
@@ -11,7 +12,7 @@ from inspect import Signature, Parameter
11
12
  from ipywidgets.widgets.trait_types import InstanceDict
12
13
 
13
14
  from ..formatters import code_css, htmlize
14
- from ..utils import html, today, _clipbox_children, _resolve_img, get_clips_dir, set_dir
15
+ from ..utils import html, today, update_class, _clipbox_children, _resolve_img, get_clips_dir, set_dir
15
16
  from . import styles, _layout
16
17
  from ..dashlab import disabled
17
18
 
@@ -126,21 +127,23 @@ class Footer(ConfigTraits):
126
127
  date = Unicode("today", allow_none=True, help="Set date string or use 'today' for current date.")
127
128
  controls = Bool(True, help="Show/hide controls like next/prev buttons and clickers in PDF/export mode.")
128
129
  progress = Bool(True, help="Show/hide progress bar at bottom of slides.")
129
-
130
+
130
131
  def _apply_change(self,change):
131
132
  # change is None when called as footer(...), so we update all things together
132
133
  self._update_footer() # html content updated
133
134
  wgts = self.main._widgets
134
135
  wgts._progbar.layout.display = "" if self.progress else "none" # show/hide progress bar
135
- wgts.iw._fpad = 23 if any([self.text, self.date, self.section, self.numbering]) else 16 # 16 is same around all sides unless footer needs more
136
136
  wgts.controls.layout.visibility = "visible" if self.controls else "hidden"
137
- wgts.iw.msg_tojs = 'RESCALE' # sets padding etc
138
137
 
139
138
  if not self.controls:
140
139
  self.main._slides.notify("Navigation controls hidden. But keyboard and edge click is still working!")
141
140
 
142
141
  for slide in self.main._slides:
143
142
  self._set_on(slide) # update footer per slide for print/export
143
+
144
+ hastext = any([self.text, self.date, self.section, self.numbering])
145
+ update_class(self.main._widgets.mainbox, 'has-footer-text', hastext)
146
+ wgts.iw.msg_tojs = 'RESCALE' # keep class update first so scale uses latest footer padding
144
147
 
145
148
  def _running_section_text(self, slide):
146
149
  "Get running section title via Slides._sectionindex and return stripped text."
@@ -192,12 +195,9 @@ class Layout(ConfigTraits):
192
195
  centered = Bool(True)
193
196
  scroll = Bool(True)
194
197
  width = Int(100)
195
- aspect = Float(16/9.001, help="Aspect ratio of slides, width/height. Add a small value to avoid exact 16/9, 16/10 etc. which causes bottom border gap in some browsers.")
198
+ aspect = Float(16/9, help="Aspect ratio of slides, width/height.")
196
199
  ncol_refs = Int(2)
197
200
  theme = Unicode('Jupyter')
198
-
199
- _reflow = False # internal use only
200
- _inotes = False # internal use only
201
201
 
202
202
  @traitlets.validate('width')
203
203
  def _limit_width(self, proposal):
@@ -206,6 +206,7 @@ class Layout(ConfigTraits):
206
206
  return proposal["value"]
207
207
 
208
208
  def _apply_change(self, change):
209
+ self._set_page_css() # set page size for print/export
209
210
  if change and change['owner'] == "theme":
210
211
  return self.main._update_theme({'owner':'layout'}) # otherwise colors don't apply to icons
211
212
  self.main._update_size(change = None) # will reset theme and send RESCALE message
@@ -217,12 +218,27 @@ class Layout(ConfigTraits):
217
218
  raise ValueError(f"Theme value expect on the followings: {themes!r}")
218
219
  self.main._widgets.theme.value = proposal["value"] # needs a robust update
219
220
  return proposal["value"]
221
+
222
+ @property
223
+ def _height(self) -> str:
224
+ "Get height of slides in mm based on aspect and width (210mm)."
225
+ return f"{round(210 / self.aspect, 2)}mm"
226
+
227
+ def _set_page_css(self):
228
+ "Set page size and margin for print/export. Must be done once per view to avoid clashes."
229
+ self.main._widgets.iw._page_css = textwrap.dedent(f'''
230
+ @page {{
231
+ margin: 0 !important;
232
+ size: 210mm {self._height} !important; /* 1pt ~ 0.35mm, so no more than one decimal required */
233
+ }}
234
+ ''')
220
235
 
221
236
  @fix_sig
222
237
  class Toggle(ConfigTraits):
223
238
  "Toggle ON/OFF checks in settings panel."
224
- reflow = Bool(False)
239
+ flow = Bool(False)
225
240
  notes = Bool(False)
241
+ inotes = Bool(False)
226
242
  toast = Bool(True)
227
243
  focus = Bool(True, help="Focus on slide area when a slide is built or rebuilt.")
228
244
  rebuild= Bool(True, help="Auto rebuild markdown slides when an included notebook-scoped variable is updated.")
@@ -233,7 +249,9 @@ class Toggle(ConfigTraits):
233
249
  if widget := getattr(self.main._widgets.checks, trait, None):
234
250
  traitlets.link((self,trait),(widget, "value"))
235
251
 
236
- def _apply_change(self, change): pass
252
+ def _apply_change(self, change):
253
+ update_class(self.main._widgets.mainbox, 'has-flow-content', self.flow) # content flow on/off
254
+ update_class(self.main._widgets.mainbox, 'has-inline-notes', self.inotes) # inline notes on/off
237
255
 
238
256
  class AutoUnicode(Unicode):
239
257
  def validate(self, obj, value):
@@ -256,7 +274,7 @@ class Logo(ConfigTraits):
256
274
  @traitlets.validate('src')
257
275
  def _fix_path(self, proposal):
258
276
  if isinstance(proposal["value"], Path):
259
- return str(Path)
277
+ return str(proposal["value"])
260
278
  return proposal["value"]
261
279
 
262
280
  def _set_props(self, **kwargs):
@@ -305,9 +323,7 @@ class Settings:
305
323
 
306
324
  self._widgets.sliders.fontsize.observe(lambda c: self.fonts(size=c.new), names=["value"])
307
325
  self._widgets.theme.observe(self._update_theme, names=["value"])
308
- self._widgets.checks.reflow.observe(self._update_theme, names=["value"]) # set reflow through layout._reflow
309
- self._widgets.checks.inotes.observe(self._update_theme, names=["value"]) # set inline notes through layout._inotes
310
- self._widgets.checks.merge.observe(self._set_merge_class, names=["value"])
326
+ self._widgets.checks.merge.observe(self._set_merge_ctx, names=["value"])
311
327
  self._widgets.buttons.print.on_click(self._print_pdf)
312
328
  self._wslider.observe(self._update_size, names=["value"])
313
329
  self._update_theme({'owner':'layout'}) # Trigger Theme with aspect changed as well
@@ -334,12 +350,10 @@ class Settings:
334
350
  raise AttributeError(f"Can't reset attribute {name!r} on {self!r}")
335
351
  self.__dict__[name] = value
336
352
 
337
- def _set_merge_class(self, change=None):
338
- if self._widgets.checks.merge.value:
339
- self._widgets.mainbox.add_class('SlidesMerged')
340
- else:
341
- self._widgets.mainbox.remove_class('SlidesMerged')
342
-
353
+ def _set_merge_ctx(self, change=None):
354
+ if curr := self._slides._current:
355
+ curr.update_display() # force update to reset frames
356
+
343
357
  def _print_pdf(self, btn):
344
358
  if self._slides._next_pending:
345
359
  return self._slides.notify(
@@ -368,7 +382,7 @@ class Settings:
368
382
  if len(self._slides) < 5 or slide.number == 0:
369
383
  return '' # no clicks for few slides or title page
370
384
 
371
- items = [getattr(item,'_sec_id','') for item in self._slides if item.index <= self._slides._lms_idx] # only before supplemnetal
385
+ items = [getattr(item,'_sec_id','') for item in self._slides if (item.index or 0) <= self._slides._lms_idx] # only before supplemnetal
372
386
  imax = len(items) - 1
373
387
  items = [items[int(round(i,0))] for i in [0, imax/4,imax/2, 3*imax/4, imax]]
374
388
  labels = '●●●●●'
@@ -400,10 +414,6 @@ class Settings:
400
414
  _layout.layout_css(self._colors['accent'],self.layout.aspect)
401
415
  ).value + _layout.loading_style # loading style should be always there
402
416
 
403
- # before applying style_css theme, set reflow and inotes
404
- self.layout._reflow = self._widgets.checks.reflow.value
405
- self.layout._inotes = self._widgets.checks.inotes.value
406
-
407
417
  theme_css = styles.style_css(**self._theme_kws)
408
418
  self._widgets.htmls.theme.value = html("style", theme_css).value
409
419
  if self._widgets.checks.notes.value:
@@ -11,7 +11,7 @@ from ipywidgets import HTML as ipwHTML, VBox
11
11
  from . import styles
12
12
  from ._layout import background_css, get_unique_css_class, loading_skeleton
13
13
  from .styles import collapse_node, hide_node
14
- from ..utils import XTML, html, _resolve_img, _styled_css, _build_css
14
+ from ..utils import XTML, html, update_class, _resolve_img, _styled_css, _build_css
15
15
  from ..xmd import capture_content
16
16
  from ..formatters import _Output, widget_from_data, slidebound
17
17
 
@@ -135,7 +135,7 @@ class Slide:
135
135
  self._has_widgets = False # Update in _build_slide function
136
136
  self._has_vars = () # Update in _slide function for markdown slides only
137
137
  self._toc_args = () # empty by default
138
- self._widget.add_class(f"n{self.number}")
138
+ self._update_class(add=f"n{self.number}")
139
139
  self._fcss = ipwHTML(layout={"margin": "0","padding": "0","heigh": "0"}) # frame separator CSS
140
140
  self._bg_ikws = {} # rebuild always re-derives background mapping from content
141
141
 
@@ -206,7 +206,7 @@ class Slide:
206
206
  # Clean up delimiters: trailing, empty, adjacent PAUSE delimiters
207
207
  self._contents = self._cleanup_delimiters(outputs)
208
208
  self._contents.extend(self._handle_refs()) # add at end if any
209
- self._set_css_classes(remove = 'Out-Sync') # Now synced
209
+ self._update_class(remove = 'Out-Sync') # Now synced
210
210
  self.update_display()
211
211
 
212
212
  if self._app.widgets.checks.focus.value: # User preference
@@ -254,8 +254,8 @@ class Slide:
254
254
  self._widget.clear_output(wait = True) # Clear, but don't go there
255
255
  # Need to know how many contents before user provide content
256
256
  with capture_content() as cap:
257
+ html('span', '', id = self._sec_id, css_class='Slide-UID').display() # to register section id in DOM
257
258
  display(VBox([
258
- html('span', '', id = self._sec_id, css_class='Slide-UID').as_widget(), # span to not occupy space, need to remove from frames later.
259
259
  self._bglayer, # background image layer
260
260
  self._ftrhtml, # dynamic footer layer
261
261
  self._fcss, # frame separator CSS
@@ -311,13 +311,9 @@ class Slide:
311
311
  if s._section:
312
312
  items.append({"c":"next", "s":s})
313
313
 
314
- first_toc = True
314
+ first_toc_slide = next((s for s in self._app[:] if s._toc_args), None)
315
315
  for s in self._app[:]:
316
- if s._toc_args and first_toc:
317
- s._widget.add_class('FirstTOC')
318
- first_toc = False
319
- else:
320
- s._widget.remove_class('FirstTOC')
316
+ update_class(s._widget, 'main-toc', s is first_toc_slide) # first TOC slide is special
321
317
 
322
318
  items = [XTML(textwrap.dedent('''
323
319
  <li class="toc-item {c}">
@@ -344,7 +340,7 @@ class Slide:
344
340
 
345
341
  self._frame_idxs = tuple(frames) if len(frames) > 1 else ()
346
342
  if self._frame_idxs:
347
- self._widget.add_class('HasFrames') # make sure class is added
343
+ self._update_class(add='HasFrames') # make sure class is added
348
344
  self._reveal_frames() # quick preview of how built frames will appear
349
345
  elif hasattr(self, '_frame_idxs'): # from previous run may be
350
346
  del self._frame_idxs
@@ -365,7 +361,8 @@ class Slide:
365
361
 
366
362
  # Next PAUSE after columns is ignored if fragments were already built from the COLUMNS output.
367
363
  if meta.get("DELIM", "") == "PAUSE":
368
- meta_prev = ensure_dict(contents[index - 1].metadata) if index - 1 >= 0 else {}
364
+ prev_index = index - 1 - offset # actual index in contents list
365
+ meta_prev = ensure_dict(contents[prev_index].metadata) if prev_index >= 0 else {}
369
366
  if "COLUMNS" in meta_prev and get_frags(meta_prev):
370
367
  continue
371
368
 
@@ -397,18 +394,12 @@ class Slide:
397
394
 
398
395
  def _update_view(self, which):
399
396
  self._set_progress()
400
- getattr(self._app.widgets.slidebox,
401
- 'remove_class' if which == 'next' else 'add_class'
402
- )('AnimPrev') # set content animation direction between parts
403
-
397
+ update_class(self._app.widgets.slidebox, 'AnimPrev', which == 'prev') # set content animation direction between parts
404
398
  frame = self._fidxs[self.indexf] if self._fidxs else {}
405
399
  self._update_transition_objs(animation=False) # avoid animations between frames
406
400
 
407
401
  if self.index == self._app.wprogress.max: # This is last slide
408
- if self.indexf + 1 == self.nf:
409
- self._app._box.add_class("InView-Last")
410
- else:
411
- self._app._box.remove_class("InView-Last")
402
+ update_class(self._app._box, "InView-Last", self.indexf + 1 == self.nf) # only if last frame
412
403
 
413
404
  self._app._send_nav_msg(
414
405
  which == 'next',
@@ -689,19 +680,13 @@ class Slide:
689
680
  if not self._app.this: # Otherwise it has side effects
690
681
  self._app.navigate_to(self.index)
691
682
 
692
- def _set_css_classes(self, add=None, remove=None):
683
+ def _update_class(self, add=None, remove=None):
693
684
  "Set CSS classes on this slide separated by space. classes are remove first and add after it."
694
685
  if remove is not None: # remove first to enable toggle
695
- if not isinstance(remove, str):
696
- raise TypeError("CSS classes should be a string with each class separated by space")
697
- for c in remove.split():
698
- self._widget.remove_class(c)
686
+ update_class(self._widget, remove, False)
699
687
 
700
- if add is not None:
701
- if not isinstance(add, str):
702
- raise TypeError("CSS classes should be a string with each class separated by space")
703
- for c in add.split():
704
- self._widget.add_class(c)
688
+ if add is not None: # add after remove to enable toggle
689
+ update_class(self._widget, add, True)
705
690
 
706
691
  @property
707
692
  def _css_class(self):