@perspective-dev/viewer 4.4.0 → 4.5.0

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 (228) hide show
  1. package/dist/cdn/perspective-viewer.js +1 -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/bootstrap.d.ts +2 -1
  24. package/dist/esm/column-format.d.ts +51 -0
  25. package/dist/esm/extensions.d.ts +6 -0
  26. package/dist/esm/perspective-viewer.d.ts +4 -1
  27. package/dist/esm/perspective-viewer.inline.js +1 -2
  28. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  29. package/dist/esm/perspective-viewer.js +1 -2
  30. package/dist/esm/perspective-viewer.js.map +4 -4
  31. package/dist/esm/perspective-viewer.worker.d.ts +2 -0
  32. package/dist/esm/plugin.d.ts +21 -77
  33. package/dist/esm/ts-rs/ColumnSelectMode.d.ts +1 -0
  34. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +77 -0
  35. package/dist/esm/ts-rs/ViewerConfig.d.ts +39 -0
  36. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -4
  37. package/dist/wasm/perspective-viewer.d.ts +88 -24
  38. package/dist/wasm/perspective-viewer.js +320 -151
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +22 -17
  41. package/package.json +24 -2
  42. package/src/css/column-selector.css +3 -2
  43. package/src/css/column-settings-panel.css +36 -6
  44. package/src/css/column-style.css +27 -2
  45. package/src/css/containers/scroll-panel.css +2 -1
  46. package/src/css/containers/tabs.css +8 -52
  47. package/src/css/dom/checkbox.css +0 -4
  48. package/src/css/form/code-editor.css +1 -0
  49. package/src/css/form/debug.css +3 -10
  50. package/src/css/plugin-selector.css +33 -0
  51. package/src/css/plugin-settings-panel.css +99 -0
  52. package/src/css/status-bar.css +1 -1
  53. package/src/css/viewer.css +65 -3
  54. package/src/rust/components/column_dropdown.rs +3 -1
  55. package/src/rust/components/column_selector/active_column.rs +13 -19
  56. package/src/rust/components/column_selector/config_selector.rs +20 -20
  57. package/src/rust/components/column_selector/filter_column.rs +14 -14
  58. package/src/rust/components/column_selector/inactive_column.rs +9 -15
  59. package/src/rust/components/column_selector/pivot_column.rs +7 -7
  60. package/src/rust/components/column_selector/sort_column.rs +7 -7
  61. package/src/rust/components/column_selector.rs +55 -37
  62. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +15 -7
  63. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +394 -0
  64. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +15 -6
  65. package/src/rust/components/column_settings_sidebar/style_tab.rs +267 -136
  66. package/src/rust/components/column_settings_sidebar.rs +43 -49
  67. package/src/rust/components/containers/dragdrop_list.rs +5 -5
  68. package/src/rust/components/containers/mod.rs +0 -1
  69. package/src/rust/components/containers/scroll_panel.rs +21 -7
  70. package/src/rust/components/containers/sidebar.rs +8 -6
  71. package/src/rust/components/containers/split_panel.rs +3 -3
  72. package/src/rust/components/containers/tab_list.rs +3 -9
  73. package/src/rust/components/copy_dropdown.rs +2 -3
  74. package/src/rust/components/datetime_column_style.rs +19 -81
  75. package/src/rust/components/editable_header.rs +2 -3
  76. package/src/rust/components/export_dropdown.rs +2 -3
  77. package/src/rust/components/expression_editor.rs +29 -17
  78. package/src/rust/components/filter_dropdown.rs +2 -1
  79. package/src/rust/components/form/color_range_selector.rs +14 -7
  80. package/src/rust/components/form/debug.rs +47 -37
  81. package/src/rust/components/main_panel.rs +24 -65
  82. package/src/rust/components/mod.rs +2 -1
  83. package/src/rust/components/number_series_style.rs +161 -0
  84. package/src/rust/components/plugin_tab.rs +221 -0
  85. package/src/rust/components/settings_panel.rs +181 -59
  86. package/src/rust/components/status_bar.rs +140 -173
  87. package/src/rust/components/status_indicator.rs +15 -22
  88. package/src/rust/components/string_column_style.rs +20 -82
  89. package/src/rust/components/style_controls/number_string_format.rs +14 -30
  90. package/src/rust/components/viewer.rs +92 -132
  91. package/src/rust/config/column_config_schema.rs +195 -0
  92. package/src/rust/config/columns_config.rs +4 -97
  93. package/src/rust/config/datetime_column_style.rs +0 -5
  94. package/src/rust/config/mod.rs +8 -2
  95. package/src/rust/config/number_series_style.rs +79 -0
  96. package/src/rust/config/plugin_static_config.rs +144 -0
  97. package/src/rust/config/string_column_style.rs +0 -5
  98. package/src/rust/config/viewer_config.rs +7 -8
  99. package/src/rust/custom_elements/copy_dropdown.rs +30 -18
  100. package/src/rust/custom_elements/debug_plugin.rs +5 -7
  101. package/src/rust/custom_elements/export_dropdown.rs +26 -18
  102. package/src/rust/custom_elements/viewer.rs +77 -77
  103. package/src/rust/custom_events.rs +181 -224
  104. package/src/rust/js/plugin.rs +45 -117
  105. package/src/rust/lib.rs +39 -5
  106. package/src/rust/presentation/drag_helpers.rs +206 -0
  107. package/src/rust/presentation/props.rs +8 -0
  108. package/src/rust/presentation.rs +256 -41
  109. package/src/rust/{tasks → queries}/column_locator.rs +17 -73
  110. package/src/rust/queries/column_values.rs +59 -0
  111. package/src/rust/{tasks → queries}/columns_iter_set.rs +11 -18
  112. package/src/rust/queries/exports.rs +96 -0
  113. package/src/rust/queries/fetch_column_stats.rs +94 -0
  114. package/src/rust/queries/get_viewer_config.rs +54 -0
  115. package/src/rust/queries/mod.rs +44 -0
  116. package/src/rust/queries/plugin_column_styles.rs +101 -0
  117. package/src/rust/{engines.rs → queries/validate_expression.rs} +26 -15
  118. package/src/rust/renderer/activate.rs +1 -0
  119. package/src/rust/renderer/limits.rs +9 -4
  120. package/src/rust/renderer/plugin_store.rs +12 -0
  121. package/src/rust/renderer/props.rs +28 -3
  122. package/src/rust/renderer/registry.rs +40 -15
  123. package/src/rust/renderer.rs +649 -55
  124. package/src/rust/session/column_defaults_update.rs +20 -28
  125. package/src/rust/session/drag_drop_update.rs +10 -10
  126. package/src/rust/session/metadata.rs +31 -16
  127. package/src/rust/session/props.rs +15 -6
  128. package/src/rust/session/view_subscription.rs +10 -0
  129. package/src/rust/session.rs +109 -147
  130. package/src/rust/tasks/copy_export.rs +178 -158
  131. package/src/rust/tasks/{structural.rs → dismiss_render_warning.rs} +20 -40
  132. package/src/rust/tasks/edit_expression.rs +68 -88
  133. package/src/rust/tasks/eject.rs +25 -22
  134. package/src/rust/tasks/intersection_observer.rs +8 -21
  135. package/src/rust/tasks/mod.rs +19 -21
  136. package/src/rust/tasks/reset_all.rs +78 -0
  137. package/src/rust/tasks/resize_observer.rs +11 -33
  138. package/src/rust/tasks/restore_and_render.rs +117 -89
  139. package/src/rust/tasks/{get_viewer_config.rs → send_column_config.rs} +38 -35
  140. package/src/rust/tasks/send_plugin_config.rs +32 -33
  141. package/src/rust/tasks/update_and_render.rs +66 -47
  142. package/src/rust/{components/containers/trap_door_panel.rs → tasks/update_theme.rs} +34 -33
  143. package/src/rust/tasks/validate_expression.rs +61 -0
  144. package/src/rust/utils/browser/selection.rs +4 -4
  145. package/src/rust/utils/mod.rs +0 -63
  146. package/src/svg/datagrid-select-row-tree.svg +13 -0
  147. package/src/svg/mega-menu-icons-density.svg +23 -0
  148. package/src/svg/mega-menu-icons-map-density.svg +24 -0
  149. package/src/svg/mega-menu-icons-map-line.svg +19 -0
  150. package/src/themes/botanical.css +27 -53
  151. package/src/themes/defaults.css +42 -36
  152. package/src/themes/dracula.css +36 -54
  153. package/src/themes/gruvbox-dark.css +39 -59
  154. package/src/themes/gruvbox.css +16 -28
  155. package/src/themes/icons.css +4 -18
  156. package/src/themes/intl/de.css +42 -6
  157. package/src/themes/intl/es.css +42 -6
  158. package/src/themes/intl/fr.css +42 -6
  159. package/src/themes/intl/ja.css +42 -6
  160. package/src/themes/intl/pt.css +42 -6
  161. package/src/themes/intl/zh.css +42 -6
  162. package/src/themes/intl.css +37 -4
  163. package/src/themes/monokai.css +45 -61
  164. package/src/themes/phosphor.css +175 -0
  165. package/src/themes/pro-dark.css +25 -34
  166. package/src/themes/solarized-dark.css +21 -36
  167. package/src/themes/solarized.css +13 -23
  168. package/src/themes/themes.css +1 -0
  169. package/src/themes/vaporwave.css +40 -74
  170. package/src/ts/bootstrap.ts +14 -3
  171. package/src/ts/column-format.ts +162 -0
  172. package/src/ts/extensions.ts +12 -1
  173. package/src/ts/perspective-viewer.ts +10 -1
  174. package/src/{rust/components/column_settings_sidebar/style_tab/stub.rs → ts/perspective-viewer.worker.ts} +2 -22
  175. package/src/ts/plugin.ts +29 -105
  176. package/src/ts/ts-rs/{FormatUnit.ts → ColumnSelectMode.ts} +1 -1
  177. package/src/ts/ts-rs/PluginStaticConfig.ts +78 -0
  178. package/src/ts/ts-rs/ViewerConfig.ts +14 -0
  179. package/src/ts/ts-rs/ViewerConfigUpdate.ts +2 -3
  180. package/dist/esm/ts-rs/ColumnConfigValues.d.ts +0 -31
  181. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +0 -1
  182. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +0 -15
  183. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +0 -18
  184. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
  185. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +0 -6
  186. package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
  187. package/dist/esm/ts-rs/FormatUnit.d.ts +0 -1
  188. package/dist/esm/ts-rs/NumberBackgroundMode.d.ts +0 -1
  189. package/dist/esm/ts-rs/NumberForegroundMode.d.ts +0 -1
  190. package/dist/esm/ts-rs/PluginConfig.d.ts +0 -2
  191. package/dist/esm/ts-rs/RoundingMode.d.ts +0 -1
  192. package/dist/esm/ts-rs/RoundingPriority.d.ts +0 -1
  193. package/dist/esm/ts-rs/SignDisplay.d.ts +0 -1
  194. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +0 -1
  195. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +0 -6
  196. package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
  197. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +0 -1
  198. package/dist/esm/ts-rs/UseGrouping.d.ts +0 -1
  199. package/src/rust/components/number_column_style.rs +0 -483
  200. package/src/rust/config/number_column_style.rs +0 -132
  201. package/src/rust/dragdrop.rs +0 -481
  202. package/src/rust/tasks/plugin_column_styles.rs +0 -98
  203. package/src/ts/ts-rs/ColumnConfigValues.ts +0 -14
  204. package/src/ts/ts-rs/CustomDatetimeFormat.ts +0 -3
  205. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +0 -5
  206. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +0 -8
  207. package/src/ts/ts-rs/DatetimeColorMode.ts +0 -3
  208. package/src/ts/ts-rs/DatetimeFormatType.ts +0 -8
  209. package/src/ts/ts-rs/FormatMode.ts +0 -3
  210. package/src/ts/ts-rs/NumberBackgroundMode.ts +0 -3
  211. package/src/ts/ts-rs/NumberForegroundMode.ts +0 -3
  212. package/src/ts/ts-rs/PluginConfig.ts +0 -4
  213. package/src/ts/ts-rs/RoundingMode.ts +0 -3
  214. package/src/ts/ts-rs/RoundingPriority.ts +0 -3
  215. package/src/ts/ts-rs/SignDisplay.ts +0 -3
  216. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +0 -3
  217. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +0 -4
  218. package/src/ts/ts-rs/StringColorMode.ts +0 -3
  219. package/src/ts/ts-rs/TrailingZeroDisplay.ts +0 -3
  220. package/src/ts/ts-rs/UseGrouping.ts +0 -3
  221. /package/dist/wasm/snippets/{perspective-viewer-68fef752754ffbc6 → perspective-viewer-39ab7da3ca157861}/inline0.js +0 -0
  222. /package/dist/wasm/snippets/{perspective-viewer-68fef752754ffbc6 → perspective-viewer-39ab7da3ca157861}/inline1.js +0 -0
  223. /package/dist/wasm/snippets/{perspective-viewer-68fef752754ffbc6 → perspective-viewer-39ab7da3ca157861}/inline2.js +0 -0
  224. /package/dist/wasm/snippets/{perspective-viewer-68fef752754ffbc6 → perspective-viewer-39ab7da3ca157861}/inline3.js +0 -0
  225. /package/dist/wasm/snippets/{perspective-viewer-68fef752754ffbc6 → perspective-viewer-39ab7da3ca157861}/inline4.js +0 -0
  226. /package/src/rust/{tasks → config}/export_method.rs +0 -0
  227. /package/src/rust/{tasks → queries}/export_app.rs +0 -0
  228. /package/src/rust/{tasks → queries}/is_invalid_drop.rs +0 -0
package/src/rust/lib.rs CHANGED
@@ -33,15 +33,16 @@ pub mod components;
33
33
  pub mod config;
34
34
  pub mod custom_elements;
35
35
  mod custom_events;
36
- mod dragdrop;
37
36
  pub mod exprtk;
38
37
  mod js;
38
+ mod presentation;
39
39
  mod root;
40
40
 
41
- pub mod engines;
42
- mod presentation;
41
+ #[doc(hidden)]
42
+ pub mod queries;
43
43
  mod renderer;
44
44
  mod session;
45
+
45
46
  #[doc(hidden)]
46
47
  pub mod tasks;
47
48
  pub mod utils;
@@ -50,6 +51,8 @@ pub mod utils;
50
51
  extern crate macro_rules_attribute;
51
52
  extern crate alloc;
52
53
 
54
+ use std::cell::RefCell;
55
+
53
56
  use perspective_js::utils::*;
54
57
  use wasm_bindgen::prelude::*;
55
58
 
@@ -63,9 +66,10 @@ use crate::utils::define_web_component;
63
66
  const TS_APPEND_CONTENT: &'static str = r#"
64
67
  import type {
65
68
  ColumnType,
66
- TableInitOptions,
69
+ TableInitOptions,
67
70
  ColumnWindow,
68
71
  ViewWindow,
72
+ TypedArrayWindow,
69
73
  OnUpdateOptions,
70
74
  JoinOptions,
71
75
  UpdateOptions,
@@ -73,6 +77,12 @@ import type {
73
77
  ViewConfigUpdate,
74
78
  SystemInfo,
75
79
  } from "@perspective-dev/client";
80
+
81
+ export type * from "../../src/ts/ts-rs/ViewerConfig.d.ts";
82
+ export type * from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
83
+ export type * from "../../src/ts/ts-rs/PluginStaticConfig.d.ts";
84
+ import type {ViewerConfig} from "../../src/ts/ts-rs/ViewerConfig.d.ts";
85
+ import type {ViewerConfigUpdate} from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
76
86
  "#;
77
87
 
78
88
  /// Register a plugin globally.
@@ -91,13 +101,37 @@ pub fn registerPlugin(name: &str) {
91
101
  /// preserve backwards-compatible synchronous API).
92
102
  #[cfg(not(feature = "external-bootstrap"))]
93
103
  #[wasm_bindgen(js_name = "init")]
94
- pub fn js_init() {
104
+ pub fn js_init(module: js_sys::WebAssembly::Module, url: web_sys::Url) {
95
105
  console_error_panic_hook::set_once();
96
106
  perspective_js::utils::set_global_logging();
97
107
  define_web_components!("export * as psp from '../../perspective-viewer.js'");
108
+ MODULE.with_borrow_mut(|f| {
109
+ *f = Some((module, url));
110
+ });
111
+
98
112
  tracing::info!("Perspective initialized.");
99
113
  }
100
114
 
115
+ thread_local! {
116
+ static MODULE: RefCell<Option<(js_sys::WebAssembly::Module, web_sys::Url)>> = RefCell::default();
117
+ }
118
+
119
+ #[cfg(not(feature = "external-bootstrap"))]
120
+ #[wasm_bindgen(js_name = "get_wasm_module")]
121
+ pub fn js_get_module() -> Result<js_sys::WebAssembly::Module, JsValue> {
122
+ MODULE
123
+ .with_borrow(|f| f.clone().map(|x| x.0))
124
+ .ok_or_else(|| "Uninited module".into())
125
+ }
126
+
127
+ #[cfg(not(feature = "external-bootstrap"))]
128
+ #[wasm_bindgen(js_name = "get_worker_url")]
129
+ pub fn js_get_worker_url() -> Result<web_sys::Url, JsValue> {
130
+ MODULE
131
+ .with_borrow(|f| f.clone().map(|x| x.1))
132
+ .ok_or_else(|| "Uninited module".into())
133
+ }
134
+
101
135
  /// Register Web Components with the global registry, given a Perspective
102
136
  /// module.
103
137
  ///
@@ -0,0 +1,206 @@
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
+ use std::cell::Cell;
14
+ use std::rc::Rc;
15
+
16
+ use perspective_client::clone;
17
+ use perspective_js::utils::*;
18
+ use wasm_bindgen::prelude::*;
19
+ use web_sys::*;
20
+ use yew::prelude::*;
21
+
22
+ use crate::js::{IntersectionObserver, IntersectionObserverEntry};
23
+
24
+ pub type DragEndCallback = Closure<dyn FnMut(DragEvent)>;
25
+
26
+ /// Safari does not set `relatedTarget` on `"dragleave"`, which makes it
27
+ /// impossible to determine whether a logical drag leave has happened with just
28
+ /// this event, so use function on `"dragenter"` to capture the `relatedTarget`.
29
+ pub fn dragenter_helper(callback: impl Fn() + 'static, target: NodeRef) -> Callback<DragEvent> {
30
+ Callback::from({
31
+ move |event: DragEvent| {
32
+ let r = (|| -> ApiResult<()> {
33
+ event.stop_propagation();
34
+ event.prevent_default();
35
+ if event.related_target().is_none() {
36
+ target
37
+ .cast::<HtmlElement>()
38
+ .into_apierror()?
39
+ .dataset()
40
+ .set("safaridragleave", "true")?;
41
+ }
42
+ Ok(())
43
+ })();
44
+
45
+ if let Err(e) = r {
46
+ web_sys::console::warn_1(&e.into());
47
+ }
48
+
49
+ callback();
50
+ }
51
+ })
52
+ }
53
+
54
+ /// HTML drag/drop will fire a bubbling `dragleave` event over all children of a
55
+ /// `dragleave`-listened-to element, so we need to filter out the events from
56
+ /// the children elements with this esoteric DOM arcana.
57
+ pub fn dragleave_helper(callback: impl Fn() + 'static, drag_ref: NodeRef) -> Callback<DragEvent> {
58
+ Callback::from({
59
+ clone!(drag_ref);
60
+ move |event: DragEvent| {
61
+ let r = (|| -> ApiResult<()> {
62
+ event.stop_propagation();
63
+ event.prevent_default();
64
+
65
+ let mut related_target = event
66
+ .related_target()
67
+ .or_else(|| Some(JsValue::UNDEFINED.unchecked_into::<EventTarget>()))
68
+ .and_then(|x| x.dyn_into::<Element>().ok());
69
+
70
+ // This is a wild chrome bug. `dragleave` can fire with the `relatedTarget`
71
+ // property set to an element inside the closed `ShadowRoot` hosted by a
72
+ // browser-native `<select>` tag, which fails the `.contains()` check
73
+ // below. This mystery `ShadowRoot` has a structure that looks like this
74
+ // (tested in Chrome 92), which we try to detect as best we can below.
75
+ //
76
+ // ```html
77
+ // <div aria-hidden="true">Selected Text Here</siv>
78
+ // <slot name="user-agent-custom-assign-slot"></slot>
79
+ // ```
80
+ //
81
+ // This is pretty course though, since there is no guarantee this structure
82
+ // will be maintained in future Chrome versions; the `.expect()` in this
83
+ // method chain should at least warn us if this regresses.
84
+ //
85
+ // Wait - you don't believe me? Throw a debugger statement inside this
86
+ // conditional and drag a column over a pivot-mode active columns list.
87
+ if related_target
88
+ .as_ref()
89
+ .map(|x| x.has_attribute("aria-hidden"))
90
+ .unwrap_or_default()
91
+ {
92
+ related_target = Some(
93
+ related_target
94
+ .into_apierror()?
95
+ .parent_node()
96
+ .into_apierror()?
97
+ .dyn_ref::<ShadowRoot>()
98
+ .ok_or_else(|| JsValue::from("Chrome drag/drop bug detection failed"))?
99
+ .host()
100
+ .unchecked_into::<Element>(),
101
+ )
102
+ }
103
+
104
+ let current_target = drag_ref.cast::<HtmlElement>().unwrap();
105
+ match related_target {
106
+ Some(ref related) => {
107
+ // Due to virtual dom these events sometimes fire after
108
+ // the node is removed ...
109
+ if !current_target.contains(Some(related))
110
+ && related.parent_element().is_some()
111
+ {
112
+ callback();
113
+ }
114
+ },
115
+ None => {
116
+ // Safari (OSX and iOS) don't set `relatedTarget`, so we need to
117
+ // read a memoized value from the `"dragenter"` event.
118
+ let dataset = current_target.dataset();
119
+ if dataset.get("safaridragleave").is_some() {
120
+ dataset.delete("safaridragleave");
121
+ } else {
122
+ callback();
123
+ }
124
+ },
125
+ };
126
+ Ok(())
127
+ })();
128
+
129
+ if let Err(e) = r {
130
+ web_sys::console::warn_1(&e.into());
131
+ }
132
+ }
133
+ })
134
+ }
135
+
136
+ #[derive(Clone)]
137
+ pub struct DragDropContainer {
138
+ pub noderef: NodeRef,
139
+ pub dragenter: Callback<DragEvent>,
140
+ pub dragleave: Callback<DragEvent>,
141
+ }
142
+
143
+ impl DragDropContainer {
144
+ pub fn new<F: Fn() + 'static, G: Fn() + 'static>(ondragenter: F, ondragleave: G) -> Self {
145
+ let noderef = NodeRef::default();
146
+ Self {
147
+ dragenter: dragenter_helper(ondragenter, noderef.clone()),
148
+ dragleave: dragleave_helper(ondragleave, noderef.clone()),
149
+ noderef,
150
+ }
151
+ }
152
+ }
153
+
154
+ /// A really, really unfortunate hack that is needed to guarantee that `dragend`
155
+ /// is called even under aggressive DOM mutation after `dragstart` is fired.
156
+ pub(super) struct DragTargetState {
157
+ target: HtmlElement,
158
+ shadow_root: ShadowRoot,
159
+ alive: Rc<Cell<bool>>,
160
+ observer: IntersectionObserver,
161
+ }
162
+
163
+ impl DragTargetState {
164
+ pub(super) fn new(host: HtmlElement, target: HtmlElement) -> Self {
165
+ let shadow_root = host.shadow_root().unwrap();
166
+ let alive = Rc::new(Cell::new(true));
167
+ let observer = IntersectionObserver::new(
168
+ &Closure::<dyn FnMut(js_sys::Array)>::new({
169
+ clone!(target, shadow_root, alive);
170
+ move |records: js_sys::Array| {
171
+ if !alive.get() {
172
+ return;
173
+ }
174
+
175
+ for record in records.iter() {
176
+ let record: IntersectionObserverEntry = record.unchecked_into();
177
+ if !record.is_intersecting() {
178
+ shadow_root.append_child(&target).unwrap();
179
+ return;
180
+ }
181
+ }
182
+ }
183
+ })
184
+ .into_js_value()
185
+ .unchecked_into(),
186
+ );
187
+
188
+ observer.observe(target.as_ref());
189
+ Self {
190
+ target,
191
+ shadow_root,
192
+ alive,
193
+ observer,
194
+ }
195
+ }
196
+ }
197
+
198
+ impl Drop for DragTargetState {
199
+ fn drop(&mut self) {
200
+ self.alive.set(false);
201
+ self.observer.unobserve(&self.target);
202
+ if self.target.is_connected() {
203
+ let _ = self.shadow_root.remove_child(&self.target);
204
+ }
205
+ }
206
+ }
@@ -13,6 +13,14 @@
13
13
  use crate::presentation::OpenColumnSettings;
14
14
  use crate::utils::PtrEqRc;
15
15
 
16
+ /// Value-semantic snapshot of the drag/drop state threaded through the
17
+ /// component tree for visual feedback (drag-highlight CSS classes).
18
+ #[derive(Clone, Debug, PartialEq, Default)]
19
+ pub struct DragDropProps {
20
+ /// Column name currently being dragged, if a drag is in progress.
21
+ pub column: Option<String>,
22
+ }
23
+
16
24
  /// Value-semantic snapshot of the presentation/UI state used by the root
17
25
  /// component to drive `is_settings_open`, `selected_theme`, and
18
26
  /// `available_themes` into child components via plain props.
@@ -11,27 +11,28 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  mod column_locator;
14
+ pub mod drag_helpers;
14
15
  mod props;
15
16
  mod sheets;
16
17
 
17
18
  use std::cell::RefCell;
18
- use std::collections::{HashMap, HashSet};
19
+ use std::collections::HashSet;
19
20
  use std::ops::Deref;
20
21
  use std::rc::Rc;
21
22
 
22
23
  use async_lock::Mutex;
23
- use perspective_js::utils::{ApiFuture, ApiResult};
24
+ use perspective_js::utils::*;
25
+ use wasm_bindgen::prelude::*;
24
26
  use web_sys::*;
25
27
  use yew::html::ImplicitClone;
26
28
  use yew::prelude::*;
27
29
 
28
30
  pub use self::column_locator::{ColumnLocator, ColumnSettingsTab, ColumnTab, OpenColumnSettings};
29
- pub use self::props::PresentationProps;
30
- use crate::config::{ColumnConfigUpdate, ColumnConfigValueUpdate, ColumnConfigValues};
31
+ use self::drag_helpers::DragTargetState;
32
+ pub use self::drag_helpers::{DragDropContainer, DragEndCallback};
33
+ pub use self::props::{DragDropProps, PresentationProps};
31
34
  use crate::utils::*;
32
35
 
33
- pub type ColumnConfigMap = HashMap<String, ColumnConfigValues>;
34
-
35
36
  /// The available themes as detected in the browser environment or set
36
37
  /// explicitly when CORS prevents detection. Detection is expensive and
37
38
  /// typically must be performed only once, when `document.styleSheets` is
@@ -41,14 +42,71 @@ struct ThemeData {
41
42
  themes: Option<Vec<String>>,
42
43
  }
43
44
 
45
+ #[derive(Clone, Debug)]
46
+ struct DragFrom {
47
+ column: String,
48
+ effect: DragEffect,
49
+ }
50
+
51
+ #[derive(Debug)]
52
+ struct DragOver {
53
+ target: DragTarget,
54
+ index: usize,
55
+ }
56
+
57
+ #[derive(Debug, Default)]
58
+ enum DragState {
59
+ #[default]
60
+ NoDrag,
61
+ DragInProgress(DragFrom),
62
+ DragOverInProgress(DragFrom, DragOver),
63
+ }
64
+
65
+ impl DragState {
66
+ const fn is_drag_in_progress(&self) -> bool {
67
+ !matches!(self, Self::NoDrag)
68
+ }
69
+ }
70
+
44
71
  /// Actual presentations tate struct with some fields hidden.
45
72
  pub struct PresentationHandle {
46
73
  viewer_elem: HtmlElement,
47
74
  theme_data: Mutex<ThemeData>,
48
75
  is_settings_open: RefCell<bool>,
49
76
  open_column_settings: RefCell<OpenColumnSettings>,
50
- columns_config: RefCell<ColumnConfigMap>,
51
77
  is_workspace: RefCell<Option<bool>>,
78
+
79
+ /// Drag/drop in-progress state. Empty (`NoDrag`) when no user drag is
80
+ /// active. Mutated by `notify_drag_*` / `notify_drop`; read by component
81
+ /// CSS-class derivations (`is_dragover`, `get_drag_column`).
82
+ drag_state: RefCell<DragState>,
83
+ pub drop_received: PubSub<(String, DragTarget, DragEffect, usize)>,
84
+
85
+ /// Injected callback from the root component fired after a drag begins
86
+ /// (one frame later, to let the drag image latch). Replaces the former
87
+ /// `dragstart_received: PubSub` field on `DragDrop`.
88
+ pub on_dragstart: RefCell<Option<Callback<DragEffect>>>,
89
+
90
+ /// Injected callback from the root component fired when the drag ends,
91
+ /// regardless of drop outcome.
92
+ pub on_dragend: RefCell<Option<Callback<()>>>,
93
+
94
+ /// Host-level `dragend` listener closure, attached to `viewer_elem` to
95
+ /// guarantee `dragend` fires even when virtual DOM updates remove the
96
+ /// dragged element from the shadow tree.
97
+ host_dragend: RefCell<Option<DragEndCallback>>,
98
+
99
+ /// IntersectionObserver-based fallback for the drag image, kept alive for
100
+ /// the duration of the drag.
101
+ drag_target: RefCell<Option<DragTargetState>>,
102
+
103
+ /// Per-element dedup cell for `perspective-config-update` event
104
+ /// dispatch. Read+written by `crate::custom_events::dispatch_*`
105
+ /// helpers; living here means every consumer with a `&Presentation`
106
+ /// (subscriptions in `wire_custom_events`, `tasks::send_plugin_config`,
107
+ /// `setSelection`) sees the same cache without separate plumbing.
108
+ pub last_dispatched_config: RefCell<Option<crate::config::ViewerConfig>>,
109
+
52
110
  pub settings_open_changed: PubSub<bool>,
53
111
 
54
112
  /// Injected callback from the root component, replacing the former
@@ -58,6 +116,11 @@ pub struct PresentationHandle {
58
116
  pub column_settings_open_changed: PubSub<(bool, Option<String>)>,
59
117
  pub theme_config_updated: PubSub<(PtrEqRc<Vec<String>>, Option<usize>)>,
60
118
  pub on_eject: PubSub<()>,
119
+
120
+ /// Fires for status-bar / main-panel pointer events that target the
121
+ /// statusbar element. `wire_custom_events` formats the `PointerEvent`'s
122
+ /// `type_()` into a `perspective-statusbar-{type}` `CustomEvent` name.
123
+ pub statusbar_pointer_event: PubSub<PointerEvent>,
61
124
  }
62
125
 
63
126
  /// State object responsible for the non-persistable/gui element state,
@@ -91,17 +154,28 @@ impl Presentation {
91
154
  settings_before_open_changed: Default::default(),
92
155
  column_settings_open_changed: Default::default(),
93
156
  on_is_workspace_changed: Default::default(),
94
- columns_config: Default::default(),
95
157
  is_settings_open: Default::default(),
96
158
  open_column_settings: Default::default(),
97
159
  theme_config_updated: PubSub::default(),
98
160
  on_eject: PubSub::default(),
161
+ statusbar_pointer_event: PubSub::default(),
162
+ last_dispatched_config: Default::default(),
163
+ drag_state: Default::default(),
164
+ drop_received: Default::default(),
165
+ on_dragstart: Default::default(),
166
+ on_dragend: Default::default(),
167
+ host_dragend: Default::default(),
168
+ drag_target: Default::default(),
99
169
  }));
100
170
 
101
171
  ApiFuture::spawn(theme.clone().init());
102
172
  theme
103
173
  }
104
174
 
175
+ pub fn viewer_elem(&self) -> &HtmlElement {
176
+ &self.viewer_elem
177
+ }
178
+
105
179
  pub fn is_visible(&self) -> bool {
106
180
  self.viewer_elem
107
181
  .offset_parent()
@@ -276,51 +350,192 @@ impl Presentation {
276
350
  Ok(true)
277
351
  }
278
352
 
279
- /// Returns an owned copy of the curent column configuration map.
280
- pub fn all_columns_configs(&self) -> ColumnConfigMap {
281
- self.columns_config.borrow().clone()
353
+ /// Snapshot the drag state as a [`DragDropProps`] value for threading
354
+ /// through the component tree without PubSub subscriptions.
355
+ pub fn drag_drop_props(&self) -> DragDropProps {
356
+ DragDropProps {
357
+ column: self.get_drag_column(),
358
+ }
282
359
  }
283
360
 
284
- pub fn reset_columns_configs(&self) {
285
- *self.columns_config.borrow_mut() = ColumnConfigMap::new();
361
+ /// Get the column name currently being drag/dropped.
362
+ pub fn get_drag_column(&self) -> Option<String> {
363
+ match *self.drag_state.borrow() {
364
+ DragState::DragInProgress(DragFrom { ref column, .. })
365
+ | DragState::DragOverInProgress(DragFrom { ref column, .. }, _) => Some(column.clone()),
366
+ _ => None,
367
+ }
286
368
  }
287
369
 
288
- /// Gets a clone of the ColumnConfig for the given column name.
289
- pub fn get_columns_config(&self, column_name: &str) -> Option<ColumnConfigValues> {
290
- self.columns_config.borrow().get(column_name).cloned()
370
+ pub fn get_drag_target(&self) -> Option<DragTarget> {
371
+ match *self.drag_state.borrow() {
372
+ DragState::DragInProgress(DragFrom {
373
+ effect: DragEffect::Move(target),
374
+ ..
375
+ })
376
+ | DragState::DragOverInProgress(
377
+ DragFrom {
378
+ effect: DragEffect::Move(target),
379
+ ..
380
+ },
381
+ _,
382
+ ) => Some(target),
383
+ _ => None,
384
+ }
291
385
  }
292
386
 
293
- /// Updates the entire column config struct. (like from a restore() call)
294
- pub fn update_columns_configs(&self, update: ColumnConfigUpdate) {
295
- match update {
296
- crate::config::OptionalUpdate::SetDefault => {
297
- let mut config = self.columns_config.borrow_mut();
298
- *config = HashMap::default()
299
- },
300
- crate::config::OptionalUpdate::Missing => {},
301
- crate::config::OptionalUpdate::Update(update) => {
302
- for (col_name, new_config) in update.into_iter() {
303
- self.columns_config
304
- .borrow_mut()
305
- .insert(col_name, new_config);
306
- }
307
- },
387
+ pub fn set_drag_image(&self, event: &DragEvent) -> ApiResult<()> {
388
+ event.stop_propagation();
389
+ if let Some(dt) = event.data_transfer() {
390
+ dt.set_drop_effect("move");
308
391
  }
392
+
393
+ let original: HtmlElement = event.target().into_apierror()?.unchecked_into();
394
+ let elem: HtmlElement = original
395
+ .children()
396
+ .get_with_index(0)
397
+ .unwrap()
398
+ .clone_node_with_deep(true)?
399
+ .unchecked_into();
400
+
401
+ elem.class_list().toggle("snap-drag-image")?;
402
+ original.append_child(&elem)?;
403
+ event.data_transfer().into_apierror()?.set_drag_image(
404
+ &elem,
405
+ event.offset_x(),
406
+ event.offset_y(),
407
+ );
408
+
409
+ *self.drag_target.borrow_mut() = Some(DragTargetState::new(
410
+ self.viewer_elem.clone(),
411
+ original.clone(),
412
+ ));
413
+
414
+ // Drag image does not register correctly unless we wait.
415
+ ApiFuture::spawn(async move {
416
+ request_animation_frame().await;
417
+ original.remove_child(&elem)?;
418
+ Ok(())
419
+ });
420
+
421
+ Ok(())
309
422
  }
310
423
 
311
- pub fn update_columns_config_value(
312
- &self,
313
- column_name: String,
314
- update: ColumnConfigValueUpdate,
315
- ) {
316
- let mut config = self.columns_config.borrow_mut();
317
- let value = config.remove(&column_name).unwrap_or_default();
318
- let update = value.update(update);
319
- if !update.is_empty() {
320
- config.insert(column_name, update);
424
+ /// Is the drag/drop state currently in `action`?
425
+ pub fn is_dragover(&self, drag_target: DragTarget) -> Option<(usize, String)> {
426
+ match *self.drag_state.borrow() {
427
+ DragState::DragOverInProgress(
428
+ DragFrom { ref column, .. },
429
+ DragOver { target, index },
430
+ ) if target == drag_target => Some((index, column.clone())),
431
+ _ => None,
432
+ }
433
+ }
434
+
435
+ pub fn notify_drop(&self, event: &DragEvent) {
436
+ event.prevent_default();
437
+ event.stop_propagation();
438
+
439
+ let action = match &*self.drag_state.borrow() {
440
+ DragState::DragOverInProgress(
441
+ DragFrom { column, effect },
442
+ DragOver { target, index },
443
+ ) => Some((column.to_string(), *target, *effect, *index)),
444
+ _ => None,
445
+ };
446
+
447
+ self.drag_target.borrow_mut().take();
448
+ *self.drag_state.borrow_mut() = DragState::NoDrag;
449
+ if let Some(action) = action {
450
+ self.drop_received.emit(action);
321
451
  }
322
452
  }
323
453
 
454
+ /// Start the drag/drop action with the name of the column being dragged.
455
+ pub fn notify_drag_start(&self, column: String, effect: DragEffect) {
456
+ *self.drag_state.borrow_mut() = DragState::DragInProgress(DragFrom { column, effect });
457
+ self.register_host_dragend();
458
+ let emit = self.on_dragstart.borrow().clone();
459
+ ApiFuture::spawn(async move {
460
+ request_animation_frame().await;
461
+ if let Some(cb) = emit {
462
+ cb.emit(effect);
463
+ }
464
+
465
+ Ok(())
466
+ });
467
+ }
468
+
469
+ /// End the drag/drop action by resetting the state to default.
470
+ pub fn notify_drag_end(&self) {
471
+ if self.drag_state.borrow().is_drag_in_progress() {
472
+ self.drag_target.borrow_mut().take();
473
+ *self.drag_state.borrow_mut() = DragState::NoDrag;
474
+ if let Some(cb) = self.on_dragend.borrow().as_ref() {
475
+ cb.emit(());
476
+ }
477
+ }
478
+ }
479
+
480
+ /// Register a `dragend` listener on the host `<perspective-viewer>`
481
+ /// element so that drag-end cleanup fires even when Yew re-renders
482
+ /// remove the original dragged element from the shadow DOM. The host
483
+ /// element is outside the virtual DOM and therefore stable.
484
+ fn register_host_dragend(&self) {
485
+ if let Some(prev) = self.host_dragend.borrow_mut().take() {
486
+ let _ = self
487
+ .viewer_elem
488
+ .remove_event_listener_with_callback("dragend", prev.as_ref().unchecked_ref());
489
+ }
490
+
491
+ let this = self.clone();
492
+ let closure = Closure::wrap(Box::new(move |_event: DragEvent| {
493
+ this.notify_drag_end();
494
+ }) as Box<dyn FnMut(DragEvent)>);
495
+
496
+ self.viewer_elem
497
+ .add_event_listener_with_callback("dragend", closure.as_ref().unchecked_ref())
498
+ .unwrap();
499
+
500
+ *self.host_dragend.borrow_mut() = Some(closure);
501
+ }
502
+
503
+ /// Leave the `action` zone.
504
+ pub fn notify_drag_leave(&self, drag_target: DragTarget) {
505
+ let reset = match *self.drag_state.borrow() {
506
+ DragState::DragOverInProgress(
507
+ DragFrom { ref column, effect },
508
+ DragOver { target, .. },
509
+ ) if target == drag_target => Some((column.clone(), effect)),
510
+ _ => None,
511
+ };
512
+
513
+ if let Some((column, effect)) = reset {
514
+ self.notify_drag_start(column, effect);
515
+ }
516
+ }
517
+
518
+ /// Enter the `action` zone at `index`, which must be <= the number of
519
+ /// children in the container.
520
+ pub fn notify_drag_enter(&self, target: DragTarget, index: usize) -> bool {
521
+ let mut drag_state = self.drag_state.borrow_mut();
522
+ let should_render = match &*drag_state {
523
+ DragState::DragOverInProgress(_, drag_to) => {
524
+ drag_to.target != target || drag_to.index != index
525
+ },
526
+ _ => true,
527
+ };
528
+
529
+ *drag_state = match &*drag_state {
530
+ DragState::DragOverInProgress(drag_from, _) | DragState::DragInProgress(drag_from) => {
531
+ DragState::DragOverInProgress(drag_from.clone(), DragOver { target, index })
532
+ },
533
+ _ => DragState::NoDrag,
534
+ };
535
+
536
+ should_render
537
+ }
538
+
324
539
  /// Snapshot the current presentation state as a [`PresentationProps`]
325
540
  /// value suitable for passing as a Yew prop. Called by the root component
326
541
  /// whenever a presentation-related PubSub event fires.