@perspective-dev/viewer 4.3.0 → 4.4.1

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 (243) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -0
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/esm/extensions.d.ts +4 -0
  24. package/dist/esm/perspective-viewer.d.ts +1 -0
  25. package/dist/esm/perspective-viewer.inline.js +2 -2
  26. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  27. package/dist/esm/perspective-viewer.js +2 -2
  28. package/dist/esm/perspective-viewer.js.map +4 -4
  29. package/dist/esm/plugin.d.ts +9 -9
  30. package/dist/esm/ts-rs/NumberForegroundMode.d.ts +1 -1
  31. package/dist/esm/ts-rs/ViewerConfig.d.ts +36 -0
  32. package/dist/wasm/perspective-viewer.d.ts +68 -59
  33. package/dist/wasm/perspective-viewer.js +216 -171
  34. package/dist/wasm/perspective-viewer.wasm +0 -0
  35. package/dist/wasm/perspective-viewer.wasm.d.ts +19 -20
  36. package/package.json +7 -5
  37. package/src/{less/aggregate-selector.less → css/aggregate-selector.css} +23 -20
  38. package/src/css/column-dropdown.css +109 -0
  39. package/src/{less/column-selector.less → css/column-selector.css} +160 -158
  40. package/src/{less/column-settings-panel.less → css/column-settings-panel.css} +70 -59
  41. package/src/{less/column-style.less → css/column-style.css} +52 -66
  42. package/src/{less/column-symbol-attributes.less → css/column-symbol-attributes.css} +15 -14
  43. package/src/{less/config-selector.less → css/config-selector.css} +151 -135
  44. package/src/{less/containers/dropdown-menu.less → css/containers/dropdown-menu.css} +20 -19
  45. package/src/{less/containers/pairs-list.less → css/containers/pairs-list.css} +13 -12
  46. package/src/{themes/variables.less → css/containers/scroll-panel.css} +25 -22
  47. package/src/{less/containers/split-panel.less → css/containers/split-panel.css} +15 -14
  48. package/src/{less/containers/tabs.less → css/containers/tabs.css} +17 -19
  49. package/src/css/dom/checkbox.css +102 -0
  50. package/src/css/dom/scrollbar.css +35 -0
  51. package/src/{less/dom/select.less → css/dom/select.css} +17 -18
  52. package/src/{less/empty-column.less → css/empty-column.css} +19 -18
  53. package/src/{less/expression-editor.less → css/expression-editor.css} +19 -18
  54. package/src/{less/filter-dropdown.less → css/filter-dropdown.css} +12 -11
  55. package/src/{less/filter-item.less → css/filter-item.css} +16 -15
  56. package/src/{less/form/code-editor.less → css/form/code-editor.css} +26 -30
  57. package/src/{less/form/debug.less → css/form/debug.css} +19 -18
  58. package/src/{less/function-dropdown.less → css/function-dropdown.css} +12 -11
  59. package/src/css/plugin-selector.css +261 -0
  60. package/src/{less/render-warning.less → css/render-warning.css} +18 -17
  61. package/src/{less/status-bar.less → css/status-bar.css} +157 -145
  62. package/src/css/type-icon.css +116 -0
  63. package/src/{less/viewer.less → css/viewer.css} +112 -146
  64. package/src/rust/components/column_dropdown.rs +229 -119
  65. package/src/rust/components/column_selector/active_column.rs +81 -62
  66. package/src/rust/components/column_selector/add_expression_button.rs +1 -0
  67. package/src/rust/components/column_selector/aggregate_selector.rs +25 -15
  68. package/src/rust/components/column_selector/config_selector.rs +315 -199
  69. package/src/rust/components/column_selector/empty_column.rs +2 -2
  70. package/src/rust/components/column_selector/expr_edit_button.rs +8 -2
  71. package/src/rust/components/column_selector/filter_column.rs +37 -26
  72. package/src/rust/components/column_selector/inactive_column.rs +41 -29
  73. package/src/rust/components/column_selector/invalid_column.rs +7 -18
  74. package/src/rust/components/column_selector/pivot_column.rs +11 -5
  75. package/src/rust/components/column_selector/sort_column.rs +23 -13
  76. package/src/rust/components/column_selector.rs +163 -84
  77. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +1 -1
  78. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +3 -2
  79. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +3 -2
  80. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +2 -3
  81. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +7 -1
  82. package/src/rust/components/column_settings_sidebar/style_tab.rs +153 -112
  83. package/src/rust/components/column_settings_sidebar.rs +91 -53
  84. package/src/rust/components/containers/dragdrop_list.rs +2 -1
  85. package/src/rust/components/containers/sidebar_close_button.rs +1 -1
  86. package/src/rust/components/containers/split_panel.rs +1 -0
  87. package/src/rust/components/containers/tab_list.rs +1 -1
  88. package/src/rust/components/copy_dropdown.rs +7 -28
  89. package/src/rust/components/datetime_column_style/custom.rs +2 -2
  90. package/src/rust/components/datetime_column_style/simple.rs +2 -2
  91. package/src/rust/components/datetime_column_style.rs +4 -2
  92. package/src/rust/components/editable_header.rs +7 -4
  93. package/src/rust/components/empty_row.rs +1 -1
  94. package/src/rust/components/export_dropdown.rs +4 -30
  95. package/src/rust/components/expression_editor.rs +19 -10
  96. package/src/rust/components/filter_dropdown.rs +246 -102
  97. package/src/rust/components/font_loader.rs +11 -28
  98. package/src/rust/components/form/code_editor.rs +17 -2
  99. package/src/rust/components/form/color_range_selector.rs +19 -6
  100. package/src/rust/components/form/debug.rs +30 -13
  101. package/src/rust/components/function_dropdown.rs +186 -113
  102. package/src/rust/components/main_panel.rs +71 -89
  103. package/src/rust/components/mod.rs +1 -1
  104. package/src/rust/components/modal.rs +7 -1
  105. package/src/rust/components/number_column_style.rs +30 -7
  106. package/src/rust/components/plugin_selector.rs +34 -102
  107. package/src/rust/components/portal.rs +274 -0
  108. package/src/rust/components/render_warning.rs +72 -123
  109. package/src/rust/components/settings_panel.rs +115 -11
  110. package/src/rust/components/status_bar.rs +222 -98
  111. package/src/rust/components/status_bar_counter.rs +8 -20
  112. package/src/rust/components/status_indicator.rs +64 -114
  113. package/src/rust/components/string_column_style.rs +2 -2
  114. package/src/rust/components/style/style_cache.rs +5 -1
  115. package/src/rust/components/viewer.rs +391 -40
  116. package/src/rust/config/number_column_style.rs +5 -1
  117. package/src/rust/config/viewer_config.rs +2 -2
  118. package/src/rust/custom_elements/copy_dropdown.rs +102 -21
  119. package/src/rust/custom_elements/debug_plugin.rs +4 -4
  120. package/src/rust/custom_elements/export_dropdown.rs +102 -20
  121. package/src/rust/custom_elements/mod.rs +0 -7
  122. package/src/rust/custom_elements/modal.rs +7 -103
  123. package/src/rust/custom_elements/viewer.rs +114 -39
  124. package/src/rust/custom_events.rs +23 -2
  125. package/src/rust/dragdrop.rs +149 -10
  126. package/src/{less/containers/scroll-panel.less → rust/engines.rs} +15 -13
  127. package/src/rust/js/plugin.rs +1 -1
  128. package/src/rust/lib.rs +10 -4
  129. package/src/rust/presentation/props.rs +39 -0
  130. package/src/rust/presentation/sheets.rs +3 -3
  131. package/src/rust/presentation.rs +44 -8
  132. package/src/rust/renderer/limits.rs +32 -3
  133. package/src/{less/dom/scrollbar.less → rust/renderer/props.rs} +18 -19
  134. package/src/rust/renderer.rs +93 -14
  135. package/src/rust/session/column_defaults_update.rs +1 -1
  136. package/src/rust/session/metadata.rs +23 -2
  137. package/src/rust/session/props.rs +178 -0
  138. package/src/rust/session.rs +124 -117
  139. package/src/rust/tasks/column_locator.rs +133 -0
  140. package/src/rust/{model → tasks}/columns_iter_set.rs +14 -23
  141. package/src/rust/{model → tasks}/edit_expression.rs +34 -10
  142. package/src/rust/{model → tasks}/eject.rs +2 -2
  143. package/src/rust/{model → tasks}/get_viewer_config.rs +0 -11
  144. package/src/rust/{model → tasks}/intersection_observer.rs +19 -3
  145. package/src/{less/containers/radio-list.less → rust/tasks/is_invalid_drop.rs} +21 -14
  146. package/src/rust/tasks/mod.rs +52 -0
  147. package/src/rust/{model → tasks}/plugin_column_styles.rs +69 -46
  148. package/src/rust/{model → tasks}/resize_observer.rs +39 -6
  149. package/src/rust/{model → tasks}/restore_and_render.rs +4 -3
  150. package/src/rust/{model → tasks}/send_plugin_config.rs +1 -1
  151. package/src/rust/tasks/structural.rs +53 -0
  152. package/src/rust/utils/mod.rs +4 -0
  153. package/src/rust/utils/modal_position.rs +110 -0
  154. package/src/rust/utils/ptr_eq_rc.rs +74 -0
  155. package/src/rust/utils/pubsub.rs +11 -1
  156. package/src/svg/bg-pattern.png +0 -0
  157. package/src/svg/close-icon.svg +1 -1
  158. package/src/svg/datagrid-select-row-tree.svg +13 -0
  159. package/src/svg/expression.svg +1 -1
  160. package/src/svg/mega-menu-icons-candlestick.svg +1 -1
  161. package/src/svg/mega-menu-icons-datagrid.svg +1 -2
  162. package/src/svg/mega-menu-icons-heatmap.svg +1 -1
  163. package/src/svg/mega-menu-icons-map-scatter.svg +1 -1
  164. package/src/svg/mega-menu-icons-ohlc.svg +1 -1
  165. package/src/svg/mega-menu-icons-sunburst.svg +1 -1
  166. package/src/svg/mega-menu-icons-treemap.svg +1 -1
  167. package/src/svg/mega-menu-icons-x-bar.svg +1 -1
  168. package/src/svg/mega-menu-icons-x-y-line.svg +1 -1
  169. package/src/svg/mega-menu-icons-x-y-scatter.svg +1 -1
  170. package/src/svg/mega-menu-icons-y-area.svg +1 -1
  171. package/src/svg/mega-menu-icons-y-bar.svg +1 -1
  172. package/src/svg/mega-menu-icons-y-line.svg +1 -1
  173. package/src/svg/mega-menu-icons-y-scatter.svg +1 -1
  174. package/src/svg/radio-hover.svg +1 -1
  175. package/src/svg/radio-off.svg +1 -1
  176. package/src/svg/radio-on.svg +1 -1
  177. package/src/themes/botanical.css +157 -0
  178. package/src/themes/defaults.css +157 -0
  179. package/src/themes/dracula.css +233 -0
  180. package/src/themes/gruvbox-dark.css +255 -0
  181. package/src/themes/gruvbox.css +134 -0
  182. package/src/themes/icons.css +107 -0
  183. package/src/themes/intl/de.css +102 -0
  184. package/src/themes/intl/es.css +102 -0
  185. package/src/themes/intl/fr.css +102 -0
  186. package/src/themes/intl/ja.css +102 -0
  187. package/src/themes/intl/pt.css +102 -0
  188. package/src/themes/intl/zh.css +102 -0
  189. package/src/themes/intl.css +102 -0
  190. package/src/themes/monokai.css +233 -0
  191. package/src/themes/phosphor.css +184 -0
  192. package/src/themes/pro-dark.css +158 -0
  193. package/src/themes/{themes.less → pro.css} +17 -21
  194. package/src/themes/solarized-dark.css +135 -0
  195. package/src/themes/solarized.css +95 -0
  196. package/src/themes/themes.css +23 -0
  197. package/src/themes/vaporwave.css +256 -0
  198. package/src/ts/extensions.ts +8 -1
  199. package/src/ts/perspective-viewer.ts +1 -0
  200. package/src/ts/plugin.ts +9 -9
  201. package/src/ts/ts-rs/NumberForegroundMode.ts +1 -1
  202. package/src/ts/ts-rs/ViewerConfig.ts +15 -0
  203. package/dist/css/variables.css +0 -0
  204. package/src/less/column-dropdown.less +0 -95
  205. package/src/less/dom/checkbox.less +0 -100
  206. package/src/less/plugin-selector.less +0 -183
  207. package/src/less/type-icon.less +0 -68
  208. package/src/rust/components/error_message.rs +0 -56
  209. package/src/rust/custom_elements/column_dropdown.rs +0 -123
  210. package/src/rust/custom_elements/filter_dropdown.rs +0 -179
  211. package/src/rust/custom_elements/function_dropdown.rs +0 -115
  212. package/src/rust/model/column_locator.rs +0 -82
  213. package/src/rust/model/is_invalid_drop.rs +0 -36
  214. package/src/rust/model/mod.rs +0 -100
  215. package/src/rust/model/reset_all.rs +0 -38
  216. package/src/rust/model/structural.rs +0 -244
  217. package/src/themes/botanical.less +0 -142
  218. package/src/themes/dracula.less +0 -101
  219. package/src/themes/gruvbox-dark.less +0 -116
  220. package/src/themes/gruvbox.less +0 -152
  221. package/src/themes/icons.less +0 -130
  222. package/src/themes/intl/de.less +0 -102
  223. package/src/themes/intl/es.less +0 -102
  224. package/src/themes/intl/fr.less +0 -102
  225. package/src/themes/intl/ja.less +0 -102
  226. package/src/themes/intl/pt.less +0 -102
  227. package/src/themes/intl/zh.less +0 -102
  228. package/src/themes/intl.less +0 -102
  229. package/src/themes/monokai.less +0 -107
  230. package/src/themes/pro-dark.less +0 -147
  231. package/src/themes/pro.less +0 -186
  232. package/src/themes/solarized-dark.less +0 -78
  233. package/src/themes/solarized.less +0 -102
  234. package/src/themes/vaporwave.less +0 -145
  235. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-d924246f0b4a3dce}/inline0.js +0 -0
  236. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-d924246f0b4a3dce}/inline1.js +0 -0
  237. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-d924246f0b4a3dce}/inline2.js +0 -0
  238. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-d924246f0b4a3dce}/inline3.js +0 -0
  239. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-d924246f0b4a3dce}/inline4.js +0 -0
  240. /package/src/rust/{model → tasks}/copy_export.rs +0 -0
  241. /package/src/rust/{model → tasks}/export_app.rs +0 -0
  242. /package/src/rust/{model → tasks}/export_method.rs +0 -0
  243. /package/src/rust/{model → tasks}/update_and_render.rs +0 -0
@@ -31,14 +31,34 @@ use crate::config::*;
31
31
  use crate::custom_events::*;
32
32
  use crate::dragdrop::*;
33
33
  use crate::js::*;
34
- use crate::model::*;
35
34
  use crate::presentation::*;
36
35
  use crate::renderer::*;
37
36
  use crate::root::Root;
38
- use crate::session::{ResetOptions, Session};
37
+ use crate::session::{ResetOptions, Session, TableLoadState};
38
+ use crate::tasks::*;
39
39
  use crate::utils::*;
40
40
  use crate::*;
41
41
 
42
+ #[wasm_bindgen]
43
+ extern "C" {
44
+ #[wasm_bindgen(typescript_type = "Promise<ViewerConfig>")]
45
+ pub type JsViewerConfigPromise;
46
+
47
+ #[wasm_bindgen(typescript_type = "ViewerConfigUpdate")]
48
+ pub type JsViewerConfigUpdate;
49
+ }
50
+
51
+ #[derive(serde::Deserialize, Default)]
52
+ struct ResizeOptions {
53
+ dimensions: Option<ResizeDimensions>,
54
+ }
55
+
56
+ #[derive(serde::Deserialize, Clone, Copy)]
57
+ struct ResizeDimensions {
58
+ width: f64,
59
+ height: f64,
60
+ }
61
+
42
62
  /// The `<perspective-viewer>` custom element.
43
63
  ///
44
64
  /// # JavaScript Examples
@@ -63,7 +83,7 @@ use crate::*;
63
83
  /// await viewer.load(worker);
64
84
  /// await viewer.restore({table: "table_one"});
65
85
  /// ```
66
- #[derive(Clone, PerspectiveProperties!)]
86
+ #[derive(Clone)]
67
87
  #[wasm_bindgen]
68
88
  pub struct PerspectiveViewerElement {
69
89
  elem: HtmlElement,
@@ -77,9 +97,41 @@ pub struct PerspectiveViewerElement {
77
97
  _subscriptions: Rc<[Subscription; 2]>,
78
98
  }
79
99
 
100
+ impl HasCustomEvents for PerspectiveViewerElement {
101
+ fn custom_events(&self) -> &CustomEvents {
102
+ &self.custom_events
103
+ }
104
+ }
105
+
106
+ impl HasPresentation for PerspectiveViewerElement {
107
+ fn presentation(&self) -> &Presentation {
108
+ &self.presentation
109
+ }
110
+ }
111
+
112
+ impl HasRenderer for PerspectiveViewerElement {
113
+ fn renderer(&self) -> &Renderer {
114
+ &self.renderer
115
+ }
116
+ }
117
+
118
+ impl HasSession for PerspectiveViewerElement {
119
+ fn session(&self) -> &Session {
120
+ &self.session
121
+ }
122
+ }
123
+
124
+ impl StateProvider for PerspectiveViewerElement {
125
+ type State = PerspectiveViewerElement;
126
+
127
+ fn clone_state(&self) -> Self::State {
128
+ self.clone()
129
+ }
130
+ }
131
+
80
132
  impl CustomElementMetadata for PerspectiveViewerElement {
81
133
  const CUSTOM_ELEMENT_NAME: &'static str = "perspective-viewer";
82
- const STATICS: &'static [&'static str] = ["registerPlugin", "getExprTKCommands"].as_slice();
134
+ const STATICS: &'static [&'static str] = ["registerPlugin"].as_slice();
83
135
  }
84
136
 
85
137
  #[wasm_bindgen]
@@ -109,7 +161,7 @@ impl PerspectiveViewerElement {
109
161
  session: session.clone(),
110
162
  renderer: renderer.clone(),
111
163
  presentation: presentation.clone(),
112
- dragdrop: DragDrop::default(),
164
+ dragdrop: DragDrop::new(&elem),
113
165
  custom_events: custom_events.clone(),
114
166
  });
115
167
 
@@ -136,7 +188,9 @@ impl PerspectiveViewerElement {
136
188
  move |_| ApiFuture::spawn(state.delete_all(&root))
137
189
  });
138
190
 
139
- let resize_handle = ResizeObserverHandle::new(&elem, &renderer, &session, &root);
191
+ let resize_handle =
192
+ ResizeObserverHandle::new(&elem, &renderer, &session, &presentation, &root);
193
+
140
194
  let intersect_handle =
141
195
  IntersectionObserverHandle::new(&elem, &presentation, &session, &renderer);
142
196
 
@@ -241,28 +295,18 @@ impl PerspectiveViewerElement {
241
295
  .unwrap_or_else(|_| js_sys::Promise::resolve(&table));
242
296
 
243
297
  let _plugin = self.renderer.get_active_plugin()?;
244
- let task = self.session.reset(ResetOptions {
298
+ let reset_task = self.session.reset(ResetOptions {
245
299
  config: true,
246
300
  expressions: true,
247
301
  stats: true,
248
- ..ResetOptions::default()
302
+ table: Some(session::TableIntermediateState::Reloaded),
249
303
  });
250
304
 
251
- let mut config = ViewConfigUpdate {
252
- columns: Some(self.session.get_view_config().columns.clone()),
253
- ..ViewConfigUpdate::default()
254
- };
255
-
256
- let metadata = self.renderer.metadata();
257
- self.session
258
- .set_update_column_defaults(&mut config, &metadata);
259
- self.session.update_view_config(config)?;
260
-
261
305
  clone!(self.renderer, self.session);
262
306
  Ok(ApiFuture::new_throttled(async move {
263
307
  let task = async {
264
308
  // Ignore this error, which is blown away by the table anyway.
265
- let _ = task.await;
309
+ let _ = reset_task.await;
266
310
  let jstable = JsFuture::from(promise)
267
311
  .await
268
312
  .map_err(|x| apierror!(TableError(x)))?;
@@ -326,7 +370,7 @@ impl PerspectiveViewerElement {
326
370
  /// await viewer.delete();
327
371
  /// ```
328
372
  pub fn delete(self) -> ApiFuture<()> {
329
- self.delete_all(self.root())
373
+ self.delete_all(&self.root)
330
374
  }
331
375
 
332
376
  /// Restart this `<perspective-viewer>` to its initial state, before
@@ -336,7 +380,7 @@ impl PerspectiveViewerElement {
336
380
  /// again, or alternatively `Self::delete` if this viewer is no longer
337
381
  /// needed.
338
382
  pub fn eject(&mut self) -> ApiFuture<()> {
339
- if self.session.has_table() {
383
+ if matches!(self.session.has_table(), Some(TableLoadState::Loaded)) {
340
384
  let mut state = Self::new_from_shadow(
341
385
  self.elem.clone(),
342
386
  self.elem.shadow_root().unwrap().unchecked_into(),
@@ -528,7 +572,7 @@ impl PerspectiveViewerElement {
528
572
  /// ```javascript
529
573
  /// await viewer.restore({group_by: ["State"]});
530
574
  /// ```
531
- pub fn restore(&self, update: JsValue) -> ApiFuture<()> {
575
+ pub fn restore(&self, update: JsViewerConfigUpdate) -> ApiFuture<()> {
532
576
  let this = self.clone();
533
577
  ApiFuture::new_throttled(async move {
534
578
  let decoded_update = ViewerConfigUpdate::decode(&update)?;
@@ -540,11 +584,26 @@ impl PerspectiveViewerElement {
540
584
  PerspectiveViewerMsg::ToggleSettingsComplete(settings, sender),
541
585
  );
542
586
 
587
+ let task = if let OptionalUpdate::Update(_) = &decoded_update.table {
588
+ Some(this.session.reset(ResetOptions {
589
+ config: true,
590
+ expressions: true,
591
+ stats: true,
592
+ ..ResetOptions::default()
593
+ }))
594
+ } else {
595
+ None
596
+ };
597
+
543
598
  let result = this
544
599
  .restore_and_render(decoded_update.clone(), {
545
600
  clone!(this, decoded_update.table);
546
601
  async move {
547
602
  if let OptionalUpdate::Update(name) = table {
603
+ if let Some(task) = task {
604
+ task.await?;
605
+ }
606
+
548
607
  this.session.set_table(name).await?;
549
608
  this.session
550
609
  .update_column_defaults(&this.renderer.metadata());
@@ -596,9 +655,9 @@ impl PerspectiveViewerElement {
596
655
  /// await viewer.restore(token);
597
656
  /// });
598
657
  /// ```
599
- pub fn save(&self) -> ApiFuture<JsValue> {
658
+ pub fn save(&self) -> JsViewerConfigPromise {
600
659
  let this = self.clone();
601
- ApiFuture::new(async move {
660
+ let fut = ApiFuture::new(async move {
602
661
  let viewer_config = this
603
662
  .renderer
604
663
  .clone()
@@ -606,7 +665,9 @@ impl PerspectiveViewerElement {
606
665
  .await?;
607
666
 
608
667
  viewer_config.encode()
609
- })
668
+ });
669
+
670
+ js_sys::Promise::from(fut).unchecked_into()
610
671
  }
611
672
 
612
673
  /// Download this viewer's internal [`View`] data via a browser download
@@ -746,24 +807,25 @@ impl PerspectiveViewerElement {
746
807
  ///
747
808
  /// # Arguments
748
809
  ///
749
- /// - `force` - If [`Self::resize`] is called with `false` or without an
750
- /// argument, and _auto-size_ mode is enabled via [`Self::setAutoSize`],
751
- /// [`Self::resize`] will log a warning and auto-disable auto-size mode.
810
+ /// - `options` - An optional object with the following fields:
811
+ /// - `dimensions` - An optional object `{width, height}` providing
812
+ /// explicit size hints (in pixels) for the plugin container. When
813
+ /// provided, the plugin element will be temporarily sized to these
814
+ /// dimensions during resize, then reset.
752
815
  ///
753
816
  /// # JavaScript Examples
754
817
  ///
755
818
  /// ```javascript
756
- /// await viewer.resize(true)
819
+ /// await viewer.resize()
820
+ /// await viewer.resize({dimensions: {width: 800, height: 600}})
757
821
  /// ```
758
822
  #[wasm_bindgen]
759
- pub fn resize(&self, force: Option<bool>) -> ApiFuture<()> {
760
- if !force.unwrap_or_default() && self.resize_handle.borrow().is_some() {
761
- let msg: JsValue = "`resize(false)` called, disabling auto-size. It can be \
762
- re-enabled with `setAutoSize(true)`."
763
- .into();
764
- web_sys::console::warn_1(&msg);
765
- *self.resize_handle.borrow_mut() = None;
766
- }
823
+ pub fn resize(&self, options: Option<JsValue>) -> ApiFuture<()> {
824
+ let opts: ResizeOptions = options
825
+ .map(|v| v.into_serde_ext())
826
+ .transpose()
827
+ .unwrap_or_default()
828
+ .unwrap_or_default();
767
829
 
768
830
  let state = self.clone_state();
769
831
  ApiFuture::new_throttled(async move {
@@ -771,6 +833,11 @@ impl PerspectiveViewerElement {
771
833
  state
772
834
  .update_and_render(ViewConfigUpdate::default())?
773
835
  .await?;
836
+ } else if let Some(dims) = opts.dimensions {
837
+ state
838
+ .renderer()
839
+ .resize_with_dimensions(dims.width, dims.height)
840
+ .await?;
774
841
  } else {
775
842
  state.renderer().resize().await?;
776
843
  }
@@ -806,6 +873,7 @@ impl PerspectiveViewerElement {
806
873
  &self.elem,
807
874
  &self.renderer,
808
875
  &self.session,
876
+ &self.presentation,
809
877
  &self.root,
810
878
  ));
811
879
  *self.resize_handle.borrow_mut() = handle;
@@ -833,7 +901,7 @@ impl PerspectiveViewerElement {
833
901
  /// viewer.setAutoPause(false);
834
902
  /// ```
835
903
  #[wasm_bindgen]
836
- pub fn setAutoPause(&self, autopause: bool) {
904
+ pub fn setAutoPause(&self, autopause: bool) -> ApiFuture<()> {
837
905
  if autopause {
838
906
  let handle = Some(IntersectionObserverHandle::new(
839
907
  &self.elem,
@@ -845,7 +913,14 @@ impl PerspectiveViewerElement {
845
913
  *self.intersection_handle.borrow_mut() = handle;
846
914
  } else {
847
915
  *self.intersection_handle.borrow_mut() = None;
916
+ if self.session.set_pause(false) {
917
+ return ApiFuture::new(
918
+ self.restore_and_render(ViewerConfigUpdate::default(), async move { Ok(()) }),
919
+ );
920
+ }
848
921
  }
922
+
923
+ ApiFuture::new(async move { Ok(()) })
849
924
  }
850
925
 
851
926
  /// Return a [`perspective_js::JsViewWindow`] for the currently selected
@@ -890,7 +965,7 @@ impl PerspectiveViewerElement {
890
965
  /// # JavaScript Examples
891
966
  ///
892
967
  /// ```javascript
893
- /// viewer.style = "--icon--color: red";
968
+ /// viewer.style = "--psp--color: red";
894
969
  /// await viewer.restyleElement();
895
970
  /// ```
896
971
  #[wasm_bindgen]
@@ -22,10 +22,10 @@ use yew::html::ImplicitClone;
22
22
 
23
23
  use crate::config::*;
24
24
  use crate::js::JsPerspectiveViewerPlugin;
25
- use crate::model::*;
26
25
  use crate::presentation::Presentation;
27
26
  use crate::renderer::*;
28
27
  use crate::session::Session;
28
+ use crate::tasks::*;
29
29
  use crate::utils::*;
30
30
  use crate::*;
31
31
 
@@ -55,7 +55,6 @@ impl Deref for CustomEventsDataRc {
55
55
  }
56
56
  }
57
57
 
58
- #[derive(PerspectiveProperties!)]
59
58
  struct CustomEventsData {
60
59
  elem: HtmlElement,
61
60
  session: Session,
@@ -64,6 +63,24 @@ struct CustomEventsData {
64
63
  last_dispatched: RefCell<Option<ViewerConfig>>,
65
64
  }
66
65
 
66
+ impl HasPresentation for CustomEventsData {
67
+ fn presentation(&self) -> &Presentation {
68
+ &self.presentation
69
+ }
70
+ }
71
+
72
+ impl HasRenderer for CustomEventsData {
73
+ fn renderer(&self) -> &Renderer {
74
+ &self.renderer
75
+ }
76
+ }
77
+
78
+ impl HasSession for CustomEventsData {
79
+ fn session(&self) -> &Session {
80
+ &self.session
81
+ }
82
+ }
83
+
67
84
  impl CustomEvents {
68
85
  pub fn new(
69
86
  elem: &HtmlElement,
@@ -171,6 +188,10 @@ impl CustomEvents {
171
188
  {
172
189
  self.0.0.dispatch_event(name, event)
173
190
  }
191
+
192
+ pub fn dispatch_raw_event(&self, event: &web_sys::CustomEvent) -> ApiResult<bool> {
193
+ self.0.0.elem.dispatch_event(event).map_err(|e| e.into())
194
+ }
174
195
  }
175
196
 
176
197
  impl CustomEventsDataRc {
@@ -10,7 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use std::cell::RefCell;
13
+ use std::cell::{Cell, RefCell};
14
14
  use std::ops::Deref;
15
15
  use std::rc::Rc;
16
16
 
@@ -20,9 +20,18 @@ use web_sys::*;
20
20
  use yew::html::ImplicitClone;
21
21
  use yew::prelude::*;
22
22
 
23
+ use crate::js::{IntersectionObserver, IntersectionObserverEntry};
23
24
  use crate::utils::*;
24
25
  use crate::*;
25
26
 
27
+ /// Value-semantic snapshot of the drag/drop state threaded through the
28
+ /// component tree for visual feedback (drag-highlight CSS classes).
29
+ #[derive(Clone, Debug, PartialEq, Default)]
30
+ pub struct DragDropProps {
31
+ /// Column name currently being dragged, if a drag is in progress.
32
+ pub column: Option<String>,
33
+ }
34
+
26
35
  #[derive(Clone, Debug)]
27
36
  struct DragFrom {
28
37
  column: String,
@@ -49,20 +58,52 @@ impl DragState {
49
58
  }
50
59
  }
51
60
 
52
- #[derive(Default)]
61
+ pub type DragEndCallback = Closure<dyn FnMut(DragEvent)>;
62
+
53
63
  pub struct DragDropState {
54
64
  drag_state: RefCell<DragState>,
55
65
  pub drop_received: PubSub<(String, DragTarget, DragEffect, usize)>,
56
- pub dragstart_received: PubSub<DragEffect>,
57
- pub dragend_received: PubSub<()>,
66
+
67
+ /// Injected callback from the root component, replacing the former
68
+ /// `dragstart_received: PubSub` field.
69
+ pub on_dragstart: RefCell<Option<Callback<DragEffect>>>,
70
+
71
+ /// Injected callback from the root component, replacing the former
72
+ /// `dragend_received: PubSub` field.
73
+ pub on_dragend: RefCell<Option<Callback<()>>>,
74
+
75
+ /// The host `<perspective-viewer>` element, used to attach the fallback
76
+ /// `dragend` listener on a stable DOM node outside the virtual DOM.
77
+ elem: HtmlElement,
78
+
79
+ /// Host-level `dragend` listener closure, stored so it can be removed
80
+ /// when a new drag starts. Attached to `elem` rather than `document`
81
+ /// to keep the listener scoped to this component instance.
82
+ host_dragend: RefCell<Option<DragEndCallback>>,
83
+
84
+ drag_target: RefCell<Option<DragTargetState>>,
58
85
  }
59
86
 
60
87
  /// The `<perspective-viewer>` drag/drop service, which manages drag/drop user
61
88
  /// interactions across components. It is a component-level service, since only
62
89
  /// one drag/drop action can be executed by the user at a time.
63
- #[derive(Clone, Default)]
90
+ #[derive(Clone)]
64
91
  pub struct DragDrop(Rc<DragDropState>);
65
92
 
93
+ impl DragDrop {
94
+ pub fn new(elem: &HtmlElement) -> Self {
95
+ Self(Rc::new(DragDropState {
96
+ drag_state: Default::default(),
97
+ drop_received: Default::default(),
98
+ on_dragstart: Default::default(),
99
+ on_dragend: Default::default(),
100
+ elem: elem.clone(),
101
+ host_dragend: Default::default(),
102
+ drag_target: Default::default(),
103
+ }))
104
+ }
105
+ }
106
+
66
107
  impl Deref for DragDrop {
67
108
  type Target = Rc<DragDropState>;
68
109
 
@@ -80,6 +121,14 @@ impl PartialEq for DragDrop {
80
121
  impl ImplicitClone for DragDrop {}
81
122
 
82
123
  impl DragDrop {
124
+ /// Snapshot the drag state as a [`DragDropProps`] value for threading
125
+ /// through the component tree without PubSub subscriptions.
126
+ pub fn to_props(&self) -> DragDropProps {
127
+ DragDropProps {
128
+ column: self.get_drag_column(),
129
+ }
130
+ }
131
+
83
132
  /// Get the column name currently being drag/dropped.
84
133
  pub fn get_drag_column(&self) -> Option<String> {
85
134
  match *self.drag_state.borrow() {
@@ -110,7 +159,7 @@ impl DragDrop {
110
159
  event.stop_propagation();
111
160
  if let Some(dt) = event.data_transfer() {
112
161
  dt.set_drop_effect("move");
113
- dt.set_data("text/plain", "{}").unwrap();
162
+ // dt.set_data("text/plain", "{}").unwrap();
114
163
  }
115
164
 
116
165
  let original: HtmlElement = event.target().into_apierror()?.unchecked_into();
@@ -129,6 +178,10 @@ impl DragDrop {
129
178
  event.offset_y(),
130
179
  );
131
180
 
181
+ *self.drag_target.borrow_mut() =
182
+ Some(DragTargetState::new(self.elem.clone(), original.clone()));
183
+
184
+ // Drag image does not register correctly unless we wait.
132
185
  ApiFuture::spawn(async move {
133
186
  request_animation_frame().await;
134
187
  original.remove_child(&elem)?;
@@ -152,6 +205,7 @@ impl DragDrop {
152
205
  pub fn notify_drop(&self, event: &DragEvent) {
153
206
  event.prevent_default();
154
207
  event.stop_propagation();
208
+
155
209
  let action = match &*self.drag_state.borrow() {
156
210
  DragState::DragOverInProgress(
157
211
  DragFrom { column, effect },
@@ -160,6 +214,7 @@ impl DragDrop {
160
214
  _ => None,
161
215
  };
162
216
 
217
+ self.drag_target.borrow_mut().take();
163
218
  *self.drag_state.borrow_mut() = DragState::NoDrag;
164
219
  if let Some(action) = action {
165
220
  self.drop_received.emit(action);
@@ -169,10 +224,14 @@ impl DragDrop {
169
224
  /// Start the drag/drop action with the name of the column being dragged.
170
225
  pub fn notify_drag_start(&self, column: String, effect: DragEffect) {
171
226
  *self.drag_state.borrow_mut() = DragState::DragInProgress(DragFrom { column, effect });
172
- let emit = self.dragstart_received.callback();
227
+ self.register_host_dragend();
228
+ let emit = self.on_dragstart.borrow().clone();
173
229
  ApiFuture::spawn(async move {
174
230
  request_animation_frame().await;
175
- emit.emit(effect);
231
+ if let Some(cb) = emit {
232
+ cb.emit(effect);
233
+ }
234
+
176
235
  Ok(())
177
236
  });
178
237
  }
@@ -180,12 +239,38 @@ impl DragDrop {
180
239
  /// End the drag/drop action by resetting the state to default.
181
240
  pub fn notify_drag_end(&self) {
182
241
  if self.drag_state.borrow().is_drag_in_progress() {
242
+ self.drag_target.borrow_mut().take();
183
243
  *self.drag_state.borrow_mut() = DragState::NoDrag;
184
- let emit = self.dragend_received.callback();
185
- emit.emit(());
244
+ if let Some(cb) = self.on_dragend.borrow().as_ref() {
245
+ cb.emit(());
246
+ }
186
247
  }
187
248
  }
188
249
 
250
+ /// Register a `dragend` listener on the host `<perspective-viewer>`
251
+ /// element so that drag-end cleanup fires even when Yew re-renders
252
+ /// remove the original dragged element from the shadow DOM. The host
253
+ /// element is outside the virtual DOM and therefore stable.
254
+ fn register_host_dragend(&self) {
255
+ // Remove any previously registered listener.
256
+ if let Some(prev) = self.host_dragend.borrow_mut().take() {
257
+ let _ = self
258
+ .elem
259
+ .remove_event_listener_with_callback("dragend", prev.as_ref().unchecked_ref());
260
+ }
261
+
262
+ let this = self.clone();
263
+ let closure = Closure::wrap(Box::new(move |_event: DragEvent| {
264
+ this.notify_drag_end();
265
+ }) as Box<dyn FnMut(DragEvent)>);
266
+
267
+ self.elem
268
+ .add_event_listener_with_callback("dragend", closure.as_ref().unchecked_ref())
269
+ .unwrap();
270
+
271
+ *self.host_dragend.borrow_mut() = Some(closure);
272
+ }
273
+
189
274
  /// Leave the `action` zone.
190
275
  pub fn notify_drag_leave(&self, drag_target: DragTarget) {
191
276
  let reset = match *self.drag_state.borrow() {
@@ -340,3 +425,57 @@ impl DragDropContainer {
340
425
  }
341
426
  }
342
427
  }
428
+
429
+ /// A really, really unfortunate hack that is needed to guarantee that `dragend`
430
+ /// is called even under aggressive DOM mutation after `dragstart` is fired.
431
+ struct DragTargetState {
432
+ target: HtmlElement,
433
+ shadow_root: ShadowRoot,
434
+ alive: Rc<Cell<bool>>,
435
+ observer: IntersectionObserver,
436
+ }
437
+
438
+ impl DragTargetState {
439
+ fn new(host: HtmlElement, target: HtmlElement) -> Self {
440
+ let shadow_root = host.shadow_root().unwrap();
441
+ let alive = Rc::new(Cell::new(true));
442
+ let observer = IntersectionObserver::new(
443
+ &Closure::<dyn FnMut(js_sys::Array)>::new({
444
+ clone!(target, shadow_root, alive);
445
+ move |records: js_sys::Array| {
446
+ if !alive.get() {
447
+ return;
448
+ }
449
+
450
+ for record in records.iter() {
451
+ let record: IntersectionObserverEntry = record.unchecked_into();
452
+ if !record.is_intersecting() {
453
+ shadow_root.append_child(&target).unwrap();
454
+ return;
455
+ }
456
+ }
457
+ }
458
+ })
459
+ .into_js_value()
460
+ .unchecked_into(),
461
+ );
462
+
463
+ observer.observe(target.as_ref());
464
+ Self {
465
+ target,
466
+ shadow_root,
467
+ alive,
468
+ observer,
469
+ }
470
+ }
471
+ }
472
+
473
+ impl Drop for DragTargetState {
474
+ fn drop(&mut self) {
475
+ self.alive.set(false);
476
+ self.observer.unobserve(&self.target);
477
+ if self.target.is_connected() {
478
+ let _ = self.shadow_root.remove_child(&self.target);
479
+ }
480
+ }
481
+ }
@@ -10,17 +10,19 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- :host {
14
- .scroll-panel-container {
15
- width: 100%;
16
- }
13
+ //! Engine-handle types for the four major state singletons.
14
+ //!
15
+ //! These are the async-machinery halves of the engine/value split described in
16
+ //! the props modules. They own JS object handles, draw locks, async
17
+ //! subscriptions, and PubSub channels — i.e. anything that cannot be cheaply
18
+ //! cloned into a plain `PartialEq` prop.
19
+ //!
20
+ //! **Current status (Step 3 of the migration):** Each `*Engine` type is a thin
21
+ //! type alias for the existing `Rc<*Handle>` wrapper. They will be replaced
22
+ //! with true struct types in later migration steps once the component tree has
23
+ //! been updated to consume value-semantic props instead of the old handles.
17
24
 
18
- .scroll-panel-auto-width {
19
- height: 1px;
20
- margin-top: -1px;
21
- }
22
-
23
- .scroll-panel-content {
24
- position: relative;
25
- }
26
- }
25
+ pub use crate::dragdrop::DragDrop as DragDropEngine;
26
+ pub use crate::presentation::Presentation as PresentationEngine;
27
+ pub use crate::renderer::Renderer as RendererEngine;
28
+ pub use crate::session::Session as SessionEngine;
@@ -141,7 +141,7 @@ impl ColumnSelectMode {
141
141
  }
142
142
  }
143
143
 
144
- #[derive(Clone, Debug, Default)]
144
+ #[derive(Clone, Debug, Default, PartialEq)]
145
145
  pub struct ViewConfigRequirements {
146
146
  pub min: Option<usize>,
147
147
  pub names: Option<Vec<String>>,