perspective_viewer/custom_elements/viewer.rs
1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13#![allow(non_snake_case)]
14
15use std::cell::RefCell;
16use std::rc::Rc;
17use std::str::FromStr;
18
19use ::perspective_js::{Table, View};
20use futures::future::join;
21use js_sys::*;
22use perspective_client::config::ViewConfigUpdate;
23use perspective_js::{JsViewWindow, apierror};
24use wasm_bindgen::JsCast;
25use wasm_bindgen::prelude::*;
26use wasm_bindgen_futures::JsFuture;
27use web_sys::*;
28use yew::prelude::*;
29
30use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg, PerspectiveViewerProps};
31use crate::config::*;
32use crate::custom_events::*;
33use crate::dragdrop::*;
34use crate::js::*;
35use crate::model::*;
36use crate::presentation::*;
37use crate::renderer::*;
38use crate::session::Session;
39use crate::utils::*;
40use crate::*;
41
42/// The `<perspective-viewer>` custom element.
43///
44/// # JavaScript Examples
45///
46/// Create a new `<perspective-viewer>`:
47///
48/// ```javascript
49/// const viewer = document.createElement("perspective-viewer");
50/// window.body.appendChild(viewer);
51/// ```
52#[derive(Clone)]
53#[wasm_bindgen]
54pub struct PerspectiveViewerElement {
55 elem: HtmlElement,
56 root: Rc<RefCell<Option<AppHandle<PerspectiveViewer>>>>,
57 resize_handle: Rc<RefCell<Option<ResizeObserverHandle>>>,
58 intersection_handle: Rc<RefCell<Option<IntersectionObserverHandle>>>,
59 session: Session,
60 renderer: Renderer,
61 presentation: Presentation,
62 events: CustomEvents,
63 _subscriptions: Rc<Subscription>,
64}
65
66derive_model!( Renderer, Session, Presentation for PerspectiveViewerElement);
67
68impl CustomElementMetadata for PerspectiveViewerElement {
69 const CUSTOM_ELEMENT_NAME: &'static str = "perspective-viewer";
70 const STATICS: &'static [&'static str] = ["registerPlugin", "getExprTKCommands"].as_slice();
71}
72
73#[wasm_bindgen]
74impl PerspectiveViewerElement {
75 #[doc(hidden)]
76 #[wasm_bindgen(constructor)]
77 pub fn new(elem: web_sys::HtmlElement) -> Self {
78 let init = web_sys::ShadowRootInit::new(web_sys::ShadowRootMode::Open);
79 let shadow_root = elem
80 .attach_shadow(&init)
81 .unwrap()
82 .unchecked_into::<web_sys::Element>();
83
84 Self::new_from_shadow(elem, shadow_root)
85 }
86
87 fn new_from_shadow(elem: web_sys::HtmlElement, shadow_root: web_sys::Element) -> Self {
88 // Application State
89 let session = Session::default();
90 let renderer = Renderer::new(&elem);
91 let presentation = Presentation::new(&elem);
92 let events = CustomEvents::new(&elem, &session, &renderer, &presentation);
93
94 // Create Yew App
95 let props = yew::props!(PerspectiveViewerProps {
96 elem: elem.clone(),
97 session: session.clone(),
98 renderer: renderer.clone(),
99 presentation: presentation.clone(),
100 dragdrop: DragDrop::default(),
101 custom_events: events.clone(),
102 weak_link: WeakScope::default(),
103 });
104
105 let root = yew::Renderer::with_root_and_props(shadow_root, props).render();
106
107 // Create callbacks
108 let update_sub = session.table_updated.add_listener({
109 clone!(renderer, session);
110 move |_| {
111 clone!(renderer, session);
112 ApiFuture::spawn(async move { renderer.update(&session).await })
113 }
114 });
115
116 let resize_handle = ResizeObserverHandle::new(&elem, &renderer, &root);
117 Self {
118 elem,
119 root: Rc::new(RefCell::new(Some(root))),
120 session,
121 renderer,
122 presentation,
123 resize_handle: Rc::new(RefCell::new(Some(resize_handle))),
124 intersection_handle: Rc::new(RefCell::new(None)),
125 events,
126 _subscriptions: Rc::new(update_sub),
127 }
128 }
129
130 #[doc(hidden)]
131 #[wasm_bindgen(js_name = "connectedCallback")]
132 pub fn connected_callback(&self) {
133 tracing::debug!("Connected <perspective-viewer>");
134 }
135
136 /// Loads a [`Table`] (or rather, a Javascript `Promise` which returns a
137 /// [`Table`]) in this viewer.
138 ///
139 /// When [`PerspectiveViewerElement::load`] resolves, the first frame of the
140 /// UI + visualization is guaranteed to have been drawn. Awaiting the result
141 /// of this method in a `try`/`catch` block will capture any errors
142 /// thrown during the loading process, or from the [`Table`] `Promise`
143 /// itself.
144 ///
145 /// A [`Table`] can be created using the
146 /// [`@finos/perspective`](https://www.npmjs.com/package/@finos/perspective)
147 /// library from NPM (see [`perspective_js`] documentation for details).
148 ///
149 /// # JavaScript Examples
150 ///
151 /// ```javascript
152 /// import perspective from "@finos/perspective";
153 ///
154 /// const worker = await perspective.worker();
155 /// viewer.load(worker.table("x,y\n1,2"));
156 /// ```
157 pub fn load(&self, table: JsValue) -> ApiFuture<()> {
158 tracing::info!("Loading Table");
159 self.session.invalidate();
160 let promise = table
161 .clone()
162 .dyn_into::<js_sys::Promise>()
163 .unwrap_or_else(|_| js_sys::Promise::resolve(&table));
164
165 self.session.reset_stats();
166 let delete_task = self.session.reset(true);
167 let mut config = ViewConfigUpdate {
168 columns: Some(self.session.get_view_config().columns.clone()),
169 ..ViewConfigUpdate::default()
170 };
171
172 self.session
173 .set_update_column_defaults(&mut config, &self.renderer.metadata());
174
175 let update_task = self.session.update_view_config(config);
176 clone!(self.renderer, self.session);
177 ApiFuture::new(async move {
178 let task = async {
179 update_task?;
180 let jstable = JsFuture::from(promise)
181 .await
182 .map_err(|x| apierror!(TableError(x)))?;
183
184 if let Some(table) =
185 wasm_bindgen_derive::try_from_js_option::<perspective_js::Table>(jstable)?
186 {
187 if let Some(existing_table) = session.get_table() {
188 if table.get_table() == &existing_table {
189 tracing::info!(
190 "Table `{}` already loaded, skipping",
191 table.get_name().await
192 );
193
194 return Ok(&session);
195 } else {
196 tracing::debug!(
197 "New table {} vs {}",
198 table.get_table().get_name(),
199 existing_table.get_name()
200 );
201 }
202 }
203
204 tracing::debug!(
205 "Successfully loaded {:.0} rows from Table",
206 table.size().await?
207 );
208
209 session.set_table(table.get_table().clone()).await?;
210 session.validate().await?.create_view().await
211 } else {
212 Err(ApiError::new("Invalid Table"))
213 }
214 };
215
216 renderer.set_throttle(None);
217 let (draw, delete) = join(renderer.draw(task), delete_task).await;
218 let result = draw.and(delete);
219 if let Err(e) = &result {
220 session.set_error(false, e.clone()).await?;
221 }
222
223 result
224 })
225 }
226
227 /// Delete the internal [`View`] and all associated state, rendering this
228 /// `<perspective-viewer>` unusable and freeing all associated resources.
229 /// Does not delete the supplied [`Table`] (as this is constructed by the
230 /// callee).
231 ///
232 /// Calling _any_ method on a `<perspective-viewer>` after [`Self::delete`]
233 /// will throw.
234 ///
235 /// <div class="warning">
236 ///
237 /// Allowing a `<perspective-viewer>` to be garbage-collected
238 /// without calling [`PerspectiveViewerElement::delete`] will leak WASM
239 /// memory!
240 ///
241 /// </div>
242 ///
243 /// # JavaScript Examples
244 ///
245 /// ```javascript
246 /// await viewer.delete();
247 /// ```
248 pub fn delete(self) -> ApiFuture<()> {
249 clone!(self.renderer, self.session, self.root);
250 ApiFuture::new(self.renderer.clone().with_lock(async move {
251 renderer.delete()?;
252 root.borrow_mut()
253 .take()
254 .ok_or("Already deleted!")?
255 .destroy();
256 session.delete().await?;
257 tracing::info!("Deleted <perspective-viewer>");
258 Ok(())
259 }))
260 }
261
262 /// Restart this `<perspective-viewer>` to its initial state, before
263 /// `load()`.
264 ///
265 /// Use `Self::restart` if you plan to call `Self::load` on this viewer
266 /// again, or alternatively `Self::delete` if this viewer is no longer
267 /// needed.
268 pub fn eject(&mut self) -> ApiFuture<()> {
269 if self.session.has_table() {
270 let mut state = Self::new_from_shadow(
271 self.elem.clone(),
272 self.elem.shadow_root().unwrap().unchecked_into(),
273 );
274
275 std::mem::swap(self, &mut state);
276 state.delete()
277 } else {
278 ApiFuture::new(async move { Ok(()) })
279 }
280 }
281
282 /// Get the underlying [`View`] for this viewer.
283 ///
284 /// Use this method to get promgrammatic access to the [`View`] as currently
285 /// configured by the user, for e.g. serializing as an
286 /// [Apache Arrow](https://arrow.apache.org/) before passing to another
287 /// library.
288 ///
289 /// The [`View`] returned by this method is owned by the
290 /// [`PerspectiveViewerElement`] and may be _invalidated_ by
291 /// [`View::delete`] at any time. Plugins which rely on this [`View`] for
292 /// their [`HTMLPerspectiveViewerPluginElement::draw`] implementations
293 /// should treat this condition as a _cancellation_ by silently aborting on
294 /// "View already deleted" errors from method calls.
295 ///
296 /// # JavaScript Examples
297 ///
298 /// ```javascript
299 /// const view = await viewer.getView();
300 /// ```
301 #[wasm_bindgen]
302 pub fn getView(&self) -> ApiFuture<View> {
303 let session = self.session.clone();
304 ApiFuture::new(async move { Ok(session.get_view().ok_or("No table set")?.into()) })
305 }
306
307 /// Get the underlying [`Table`] for this viewer (as passed to
308 /// [`PerspectiveViewerElement::load`]).
309 ///
310 /// # Arguments
311 ///
312 /// - `wait_for_table` - whether to wait for
313 /// [`PerspectiveViewerElement::load`] to be called, or fail immediately
314 /// if [`PerspectiveViewerElement::load`] has not yet been called.
315 ///
316 /// # JavaScript Examples
317 ///
318 /// ```javascript
319 /// const table = await viewer.getTable();
320 /// ```
321 #[wasm_bindgen]
322 pub fn getTable(&self, wait_for_table: Option<bool>) -> ApiFuture<Table> {
323 let session = self.session.clone();
324 ApiFuture::new(async move {
325 match session.get_table() {
326 Some(table) => Ok(table.into()),
327 None if !wait_for_table.unwrap_or_default() => Err("No table set".into()),
328 None => {
329 session.table_loaded.listen_once().await?;
330 Ok(session.get_table().ok_or("No table set")?.into())
331 },
332 }
333 })
334 }
335
336 /// Get render statistics. Some fields of the returned stats object are
337 /// relative to the last time [`PerspectiveViewerElement::getRenderStats`]
338 /// was called, ergo calling this method resets these fields.
339 ///
340 /// # JavaScript Examples
341 ///
342 /// ```javascript
343 /// const {virtual_fps, actual_fps} = await viewer.getRenderStats();
344 /// ```
345 #[wasm_bindgen]
346 pub fn getRenderStats(&self) -> ApiResult<JsValue> {
347 Ok(JsValue::from_serde_ext(
348 &self.renderer.render_timer().get_stats(),
349 )?)
350 }
351
352 /// Flush any pending modifications to this `<perspective-viewer>`. Since
353 /// `<perspective-viewer>`'s API is almost entirely `async`, it may take
354 /// some milliseconds before any user-initiated changes to the [`View`]
355 /// affects the rendered element. If you want to make sure all pending
356 /// actions have been rendered, call and await [`Self::flush`].
357 ///
358 /// [`Self::flush`] will resolve immediately if there is no [`Table`] set.
359 ///
360 /// # JavaScript Examples
361 ///
362 /// In this example, [`Self::restore`] is called without `await`, but the
363 /// eventual render which results from this call can still be awaited by
364 /// immediately awaiting [`Self::flush`] instead.
365 ///
366 /// ```javascript
367 /// viewer.restore(config);
368 /// await viewer.flush();
369 /// ```
370 pub fn flush(&self) -> ApiFuture<()> {
371 clone!(self.renderer);
372 ApiFuture::new(async move {
373 request_animation_frame().await;
374 renderer.with_lock(async { Ok(()) }).await
375 })
376 }
377
378 /// Restores this element from a full/partial
379 /// [`perspective_js::JsViewConfig`].
380 ///
381 /// One of the best ways to use [`Self::restore`] is by first configuring
382 /// a `<perspective-viewer>` as you wish, then using either the `Debug`
383 /// panel or "Copy" -> "config.json" from the toolbar menu to snapshot
384 /// the [`Self::restore`] argument as JSON.
385 ///
386 /// # Arguments
387 ///
388 /// - `update` - The config to restore to, as returned by [`Self::save`] in
389 /// either "json", "string" or "arraybuffer" format.
390 ///
391 /// # JavaScript Examples
392 ///
393 /// Apply a `group_by` to the current [`View`], without modifying/resetting
394 /// other fields:
395 ///
396 /// ```javascript
397 /// await viewer.restore({group_by: ["State"]});
398 /// ```
399 pub fn restore(&self, update: JsValue) -> ApiFuture<()> {
400 tracing::info!("Restoring ViewerConfig");
401 let this = self.clone();
402 ApiFuture::new(async move {
403 let decoded_update = ViewerConfigUpdate::decode(&update)?;
404 let root = this.root.clone();
405 let settings = decoded_update.settings.clone();
406 let result = root
407 .borrow()
408 .as_ref()
409 .into_apierror()?
410 .send_message_async(move |x| {
411 PerspectiveViewerMsg::ToggleSettingsComplete(settings, x)
412 });
413
414 let task = this
415 .restore_and_render(decoded_update, async move { Ok(result.await?) })
416 .await;
417
418 if let Err(e) = task {
419 this.session().set_error(false, e.clone()).await?;
420 Err(e)
421 } else {
422 Ok(())
423 }
424 })
425 }
426
427 pub fn resetError(&self) -> ApiFuture<()> {
428 self.session.invalidate();
429 let this = self.clone();
430 ApiFuture::new(async move {
431 this.update_and_render(ViewConfigUpdate::default())?.await?;
432 Ok(())
433 })
434 }
435
436 /// Save this element to serialized state object, one which can be restored
437 /// via the [`Self::restore`] method.
438 ///
439 /// # Arguments
440 ///
441 /// - `format` - Supports "json" (default), "arraybuffer" or "string".
442 ///
443 /// # JavaScript Examples
444 ///
445 /// Get the current `group_by` setting:
446 ///
447 /// ```javascript
448 /// const {group_by} = await viewer.restore();
449 /// ```
450 ///
451 /// Reset workflow attached to an external button `myResetButton`:
452 ///
453 /// ```javascript
454 /// const token = await viewer.save();
455 /// myResetButton.addEventListener("clien", async () => {
456 /// await viewer.restore(token);
457 /// });
458 /// ```
459 pub fn save(&self, format: Option<String>) -> ApiFuture<JsValue> {
460 let this = self.clone();
461 ApiFuture::new(async move {
462 let viewer_config_task = this.get_viewer_config();
463 let format = format
464 .as_ref()
465 .map(|x| ViewerConfigEncoding::from_str(x))
466 .transpose()?;
467
468 let viewer_config = viewer_config_task.await?;
469 viewer_config.encode(&format)
470 })
471 }
472
473 /// Download this viewer's internal [`View`] data as a `.csv` file.
474 ///
475 /// # Arguments
476 ///
477 /// - `flat` - Whether to use the current [`perspective_js::JsViewConfig`]
478 /// to generate this data, or use the default.
479 ///
480 /// # JavaScript Examples
481 ///
482 /// ```javascript
483 /// myDownloadButton.addEventListener("click", async () => {
484 /// await viewer.download();
485 /// })
486 /// ```
487 pub fn download(&self, flat: Option<bool>) -> ApiFuture<()> {
488 let session = self.session.clone();
489 ApiFuture::new(async move {
490 let val = session
491 .csv_as_jsvalue(flat.unwrap_or_default(), None)
492 .await?
493 .as_blob()?;
494
495 // TODO name.as_deref().unwrap_or("untitled.csv")
496 download("untitled.csv", &val)
497 })
498 }
499
500 /// Copy this viewer's `View` or `Table` data as CSV to the system
501 /// clipboard.
502 ///
503 /// # Arguments
504 ///
505 /// - `method` - The `ExportMethod` (serialized as a `String`) to use to
506 /// render the data to the Clipboard.
507 ///
508 /// # JavaScript Examples
509 ///
510 /// ```javascript
511 /// myDownloadButton.addEventListener("click", async () => {
512 /// await viewer.copy();
513 /// })
514 /// ```
515 pub fn copy(&self, method: Option<JsString>) -> ApiFuture<()> {
516 let this = self.clone();
517 ApiFuture::new(async move {
518 let method = if let Some(method) = method
519 .map(|x| x.unchecked_into())
520 .map(serde_wasm_bindgen::from_value)
521 {
522 method?
523 } else {
524 ExportMethod::Csv
525 };
526
527 let js_task = this.export_method_to_jsvalue(method);
528 copy_to_clipboard(js_task, MimeType::TextPlain).await
529 })
530 }
531
532 /// Reset the viewer's `ViewerConfig` to the default.
533 ///
534 /// # Arguments
535 ///
536 /// - `reset_all` - If set, will clear expressions and column settings as
537 /// well.
538 ///
539 /// # JavaScript Examples
540 ///
541 /// ```javascript
542 /// await viewer.reset();
543 /// ```
544 pub fn reset(&self, reset_all: Option<bool>) -> ApiFuture<()> {
545 tracing::info!("Resetting config");
546 let root = self.root.clone();
547 let all = reset_all.unwrap_or_default();
548 ApiFuture::new(async move {
549 let task = root
550 .borrow()
551 .as_ref()
552 .ok_or("Already deleted")?
553 .send_message_async(move |x| PerspectiveViewerMsg::Reset(all, Some(x)));
554
555 Ok(task.await?)
556 })
557 }
558
559 /// Recalculate the viewer's dimensions and redraw.
560 ///
561 /// Use this method to tell `<perspective-viewer>` its dimensions have
562 /// changed when auto-size mode has been disabled via [`Self::setAutoSize`].
563 /// [`Self::resize`] resolves when the resize-initiated redraw of this
564 /// element has completed.
565 ///
566 /// # Arguments
567 ///
568 /// - `force` - If [`Self::resize`] is called with `false` or without an
569 /// argument, and _auto-size_ mode is enabled via [`Self::setAutoSize`],
570 /// [`Self::resize`] will log a warning and auto-disable auto-size mode.
571 ///
572 /// # JavaScript Examples
573 ///
574 /// ```javascript
575 /// await viewer.resize(true)
576 /// ```
577 #[wasm_bindgen]
578 pub fn resize(&self, force: Option<bool>) -> ApiFuture<()> {
579 if !force.unwrap_or_default() && self.resize_handle.borrow().is_some() {
580 let msg: JsValue = "`resize(false)` called, disabling auto-size. It can be \
581 re-enabled with `setAutoSize(true)`."
582 .into();
583 web_sys::console::warn_1(&msg);
584 *self.resize_handle.borrow_mut() = None;
585 }
586
587 let renderer = self.renderer.clone();
588 ApiFuture::new(async move { renderer.resize().await })
589 }
590
591 /// Sets the auto-size behavior of this component.
592 ///
593 /// When `true`, this `<perspective-viewer>` will register a
594 /// `ResizeObserver` on itself and call [`Self::resize`] whenever its own
595 /// dimensions change. However, when embedded in a larger application
596 /// context, you may want to call [`Self::resize`] manually to avoid
597 /// over-rendering; in this case auto-sizing can be disabled via this
598 /// method. Auto-size behavior is enabled by default.
599 ///
600 /// # Arguments
601 ///
602 /// - `autosize` - Whether to enable `auto-size` behavior or not.
603 ///
604 /// # JavaScript Examples
605 ///
606 /// Disable auto-size behavior:
607 ///
608 /// ```javascript
609 /// viewer.setAutoSize(false);
610 /// ```
611 #[wasm_bindgen]
612 pub fn setAutoSize(&self, autosize: bool) {
613 if autosize {
614 let handle = Some(ResizeObserverHandle::new(
615 &self.elem,
616 &self.renderer,
617 self.root.borrow().as_ref().unwrap(),
618 ));
619 *self.resize_handle.borrow_mut() = handle;
620 } else {
621 *self.resize_handle.borrow_mut() = None;
622 }
623 }
624
625 /// Sets the auto-pause behavior of this component.
626 ///
627 /// When `true`, this `<perspective-viewer>` will register an
628 /// `IntersectionObserver` on itself and subsequently skip rendering
629 /// whenever its viewport visibility changes. Auto-pause is enabled by
630 /// default.
631 ///
632 /// # Arguments
633 ///
634 /// - `autopause` Whether to enable `auto-pause` behavior or not.
635 ///
636 /// # JavaScript Examples
637 ///
638 /// Disable auto-size behavior:
639 ///
640 /// ```javascript
641 /// viewer.setAutoPause(false);
642 /// ```
643 #[wasm_bindgen]
644 pub fn setAutoPause(&self, autopause: bool) {
645 if autopause {
646 let handle = Some(IntersectionObserverHandle::new(
647 &self.elem,
648 &self.session,
649 &self.renderer,
650 ));
651 *self.intersection_handle.borrow_mut() = handle;
652 } else {
653 *self.intersection_handle.borrow_mut() = None;
654 }
655 }
656
657 /// Return a [`perspective_js::JsViewWindow`] for the currently selected
658 /// region.
659 #[wasm_bindgen]
660 pub fn getSelection(&self) -> Option<JsViewWindow> {
661 self.renderer.get_selection().map(|x| x.into())
662 }
663
664 /// Set the selection [`perspective_js::JsViewWindow`] for this element.
665 #[wasm_bindgen]
666 pub fn setSelection(&self, window: Option<JsViewWindow>) -> ApiResult<()> {
667 let window = window.map(|x| x.into_serde_ext()).transpose()?;
668 if self.renderer.get_selection() != window {
669 self.events.dispatch_select(window.as_ref())?;
670 }
671
672 self.renderer.set_selection(window);
673 Ok(())
674 }
675
676 /// Get this viewer's edit port for the currently loaded [`Table`] (see
677 /// [`Table::update`] for details on ports).
678 #[wasm_bindgen]
679 pub fn getEditPort(&self) -> Result<f64, JsValue> {
680 self.session
681 .metadata()
682 .get_edit_port()
683 .ok_or_else(|| "No `Table` loaded".into())
684 }
685
686 /// Restyle all plugins from current document.
687 ///
688 /// <div class="warning">
689 ///
690 /// [`Self::restyleElement`] _must_ be called for many runtime changes to
691 /// CSS properties to be reflected in an already-rendered
692 /// `<perspective-viewer>`.
693 ///
694 /// </div>
695 ///
696 /// # JavaScript Examples
697 ///
698 /// ```javascript
699 /// viewer.style = "--icon--color: red";
700 /// await viewer.restyleElement();
701 /// ```
702 #[wasm_bindgen]
703 pub fn restyleElement(&self) -> ApiFuture<JsValue> {
704 clone!(self.renderer, self.session);
705 ApiFuture::new(async move {
706 let view = session.get_view().into_apierror()?;
707 renderer.restyle_all(&view).await
708 })
709 }
710
711 /// Set the available theme names available in the status bar UI.
712 ///
713 /// Calling [`Self::resetThemes`] may cause the current theme to switch,
714 /// if e.g. the new theme set does not contain the current theme.
715 ///
716 /// # JavaScript Examples
717 ///
718 /// Restrict `<perspective-viewer>` theme options to _only_ default light
719 /// and dark themes, regardless of what is auto-detected from the page's
720 /// CSS:
721 ///
722 /// ```javascript
723 /// viewer.resetThemes(["Pro Light", "Pro Dark"])
724 /// ```
725 #[wasm_bindgen]
726 pub fn resetThemes(&self, themes: Option<Box<[JsValue]>>) -> ApiFuture<JsValue> {
727 clone!(self.renderer, self.session, self.presentation);
728 ApiFuture::new(async move {
729 let themes: Option<Vec<String>> = themes
730 .unwrap_or_default()
731 .iter()
732 .map(|x| x.as_string())
733 .collect();
734
735 let theme_name = presentation.get_selected_theme_name().await;
736 let mut changed = presentation.reset_available_themes(themes).await;
737 let reset_theme = presentation
738 .get_available_themes()
739 .await?
740 .iter()
741 .find(|y| theme_name.as_ref() == Some(y))
742 .cloned();
743
744 changed = presentation.set_theme_name(reset_theme.as_deref()).await? || changed;
745 if changed && let Some(view) = session.get_view() {
746 return renderer.restyle_all(&view).await;
747 }
748
749 Ok(JsValue::UNDEFINED)
750 })
751 }
752
753 /// Determines the render throttling behavior. Can be an integer, for
754 /// millisecond window to throttle render event; or, if `None`, adaptive
755 /// throttling will be calculated from the measured render time of the
756 /// last 5 frames.
757 ///
758 /// # Arguments
759 ///
760 /// - `throttle` - The throttle rate in milliseconds (f64), or `None` for
761 /// adaptive throttling.
762 ///
763 /// # JavaScript Examples
764 ///
765 /// Only draws at most 1 frame/sec:
766 ///
767 /// ```rust
768 /// viewer.setThrottle(1000);
769 /// ```
770 #[wasm_bindgen]
771 pub fn setThrottle(&self, val: Option<f64>) {
772 self.renderer.set_throttle(val);
773 }
774
775 /// Toggle (or force) the config panel open/closed.
776 ///
777 /// # Arguments
778 ///
779 /// - `force` - Force the state of the panel open or closed, or `None` to
780 /// toggle.
781 ///
782 /// # JavaScript Examples
783 ///
784 /// ```javascript
785 /// await viewer.toggleConfig();
786 /// ```
787 #[wasm_bindgen]
788 pub fn toggleConfig(&self, force: Option<bool>) -> ApiFuture<JsValue> {
789 let root = self.root.clone();
790 ApiFuture::new(async move {
791 let force = force.map(SettingsUpdate::Update);
792 let task = root
793 .borrow()
794 .as_ref()
795 .into_apierror()?
796 .send_message_async(|x| PerspectiveViewerMsg::ToggleSettingsInit(force, Some(x)));
797
798 task.await.map_err(|_| JsValue::from("Cancelled"))?
799 })
800 }
801
802 /// Get an `Array` of all of the plugin custom elements registered for this
803 /// element. This may not include plugins which called
804 /// [`registerPlugin`] after the host has rendered for the first time.
805 #[wasm_bindgen]
806 pub fn getAllPlugins(&self) -> Array {
807 self.renderer.get_all_plugins().iter().collect::<Array>()
808 }
809
810 /// Gets a plugin Custom Element with the `name` field, or get the active
811 /// plugin if no `name` is provided.
812 ///
813 /// # Arguments
814 ///
815 /// - `name` - The `name` property of a perspective plugin Custom Element,
816 /// or `None` for the active plugin's Custom Element.
817 #[wasm_bindgen]
818 pub fn getPlugin(&self, name: Option<String>) -> ApiResult<JsPerspectiveViewerPlugin> {
819 match name {
820 None => self.renderer.get_active_plugin(),
821 Some(name) => self.renderer.get_plugin(&name),
822 }
823 }
824
825 /// Create a new JavaScript Heap reference for this model instance.
826 #[doc(hidden)]
827 #[allow(clippy::use_self)]
828 #[wasm_bindgen]
829 pub fn get_model(&self) -> PerspectiveViewerElement {
830 self.clone()
831 }
832
833 /// Asynchronously opens the column settings for a specific column.
834 /// When finished, the `<perspective-viewer>` element will emit a
835 /// "perspective-toggle-column-settings" CustomEvent.
836 /// The event's details property has two fields: `{open: bool, column_name?:
837 /// string}`. The CustomEvent is also fired whenever the user toggles the
838 /// sidebar manually.
839 #[wasm_bindgen]
840 pub fn toggleColumnSettings(&self, column_name: String) -> ApiFuture<()> {
841 clone!(self.session, self.root);
842 ApiFuture::new(async move {
843 let locator = session.metadata().get_column_locator(Some(column_name));
844 let task = root
845 .borrow()
846 .as_ref()
847 .into_apierror()?
848 .send_message_async(|sender| PerspectiveViewerMsg::OpenColumnSettings {
849 locator,
850 sender: Some(sender),
851 toggle: true,
852 });
853 task.await.map_err(|_| ApiError::from("Cancelled"))
854 })
855 }
856
857 /// Force open the settings for a particular column. Pass `null` to close
858 /// the column settings panel. See [`Self::toggleColumnSettings`] for more.
859 #[wasm_bindgen]
860 pub fn openColumnSettings(
861 &self,
862 column_name: Option<String>,
863 toggle: Option<bool>,
864 ) -> ApiFuture<()> {
865 clone!(self.session, self.root);
866 ApiFuture::new(async move {
867 let locator = session.metadata().get_column_locator(column_name);
868 let task = root
869 .borrow()
870 .as_ref()
871 .into_apierror()?
872 .send_message_async(|sender| PerspectiveViewerMsg::OpenColumnSettings {
873 locator,
874 sender: Some(sender),
875 toggle: toggle.unwrap_or_default(),
876 });
877 task.await.map_err(|_| ApiError::from("Cancelled"))
878 })
879 }
880}