@perspective-dev/viewer 4.5.2 → 5.0.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.
- package/dist/cdn/perspective-viewer.js +20 -1
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/esm/extensions.d.ts +22 -3
- package/dist/esm/perspective-viewer.d.ts +9 -0
- package/dist/esm/perspective-viewer.inline.js +20 -1
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +20 -1
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/plugin.d.ts +71 -5
- package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
- package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
- package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
- package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
- package/dist/esm/ts-rs/Layout.d.ts +11 -0
- package/dist/esm/ts-rs/Orientation.d.ts +4 -0
- package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
- package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
- package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
- package/dist/wasm/perspective-viewer.d.ts +211 -77
- package/dist/wasm/perspective-viewer.js +528 -180
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
- package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_context_menu.css +14 -0
- package/src/css/column-settings-panel.css +1 -1
- package/src/css/containers/context-menu.css +101 -0
- package/src/css/panel-tab.css +193 -0
- package/src/css/plugin-selector.css +7 -10
- package/src/css/status-bar.css +75 -81
- package/src/css/viewer.css +180 -70
- package/src/rust/components/column_dropdown.rs +40 -30
- package/src/rust/components/column_selector/active_column.rs +7 -18
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
- package/src/rust/components/column_selector/config_selector.rs +33 -81
- package/src/rust/components/column_selector/filter_column.rs +5 -15
- package/src/rust/components/column_selector/inactive_column.rs +3 -8
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +5 -11
- package/src/rust/components/containers/split_panel.rs +175 -62
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/form/debug.rs +3 -0
- package/src/rust/components/global_filter_bar.rs +76 -0
- package/src/rust/components/main_panel/frame_theme.rs +186 -0
- package/src/rust/components/main_panel/msg.rs +62 -0
- package/src/rust/components/main_panel/presize.rs +220 -0
- package/src/rust/components/main_panel/reconcile.rs +224 -0
- package/src/rust/components/main_panel/render.rs +385 -0
- package/src/rust/components/main_panel/update.rs +184 -0
- package/src/rust/components/main_panel.rs +311 -71
- package/src/rust/components/mod.rs +4 -0
- package/src/rust/components/panel_menu.rs +486 -0
- package/src/rust/components/panel_tab.rs +505 -0
- package/src/rust/components/plugin_tab.rs +1 -1
- package/src/rust/components/settings_panel.rs +13 -5
- package/src/rust/components/status_bar.rs +110 -178
- package/src/rust/components/status_indicator.rs +6 -9
- package/src/rust/components/style/mod.rs +5 -0
- package/src/rust/components/style/style_provider.rs +2 -0
- package/src/rust/components/viewer/filters.rs +133 -0
- package/src/rust/components/viewer/msg.rs +180 -0
- package/src/rust/components/viewer/panels.rs +326 -0
- package/src/rust/components/viewer/render.rs +243 -0
- package/src/rust/components/viewer/settings.rs +402 -0
- package/src/rust/components/viewer/snapshots.rs +134 -0
- package/src/rust/components/viewer/wiring.rs +505 -0
- package/src/rust/components/viewer.rs +243 -756
- package/src/rust/config/export_method.rs +1 -1
- package/src/rust/config/mod.rs +4 -0
- package/src/rust/config/options.rs +71 -0
- package/src/rust/config/viewer_config.rs +24 -2
- package/src/rust/config/workspace_config.rs +92 -0
- package/src/rust/custom_elements/copy_dropdown.rs +3 -1
- package/src/rust/custom_elements/debug_plugin.rs +20 -1
- package/src/rust/custom_elements/export_dropdown.rs +3 -1
- package/src/rust/custom_elements/viewer.rs +1015 -286
- package/src/rust/custom_events.rs +212 -61
- package/src/rust/js/mod.rs +2 -0
- package/src/rust/js/plugin.rs +297 -10
- package/src/rust/js/regular_layout.rs +258 -0
- package/src/rust/lib.rs +20 -1
- package/src/rust/presentation.rs +66 -33
- package/src/rust/queries/get_viewer_config.rs +29 -11
- package/src/rust/queries/plugin_column_styles.rs +2 -2
- package/src/rust/renderer/activate.rs +41 -14
- package/src/rust/renderer/dispatch.rs +436 -0
- package/src/rust/renderer/plugin_config.rs +495 -0
- package/src/rust/renderer/registry.rs +18 -6
- package/src/rust/renderer/render_timer.rs +29 -10
- package/src/rust/renderer.rs +350 -837
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +753 -264
- package/src/rust/tasks/apply_global_filters.rs +89 -0
- package/src/rust/tasks/auto_pause.rs +171 -0
- package/src/rust/tasks/copy_export.rs +29 -19
- package/src/rust/tasks/create_panel.rs +243 -0
- package/src/rust/tasks/dismiss_render_warning.rs +3 -1
- package/src/rust/tasks/edit_expression.rs +4 -4
- package/src/rust/tasks/eject.rs +64 -23
- package/src/rust/tasks/mod.rs +28 -4
- package/src/rust/tasks/pipeline.rs +411 -0
- package/src/rust/tasks/presize_panels.rs +324 -0
- package/src/rust/tasks/reset_all.rs +22 -12
- package/src/rust/tasks/resize_observer.rs +124 -87
- package/src/rust/tasks/restore_and_render.rs +80 -86
- package/src/rust/tasks/restore_panel.rs +145 -0
- package/src/rust/tasks/send_column_config.rs +5 -2
- package/src/rust/tasks/send_plugin_config.rs +5 -2
- package/src/rust/tasks/set_edit_mode.rs +59 -0
- package/src/rust/tasks/sync_update_panels.rs +152 -0
- package/src/rust/tasks/table_lifecycle.rs +158 -0
- package/src/rust/tasks/update_theme.rs +56 -12
- package/src/rust/utils/completion.rs +56 -0
- package/src/rust/utils/custom_element.rs +5 -0
- package/src/rust/utils/debounce.rs +130 -7
- package/src/rust/utils/mod.rs +6 -0
- package/src/rust/utils/pending_effects.rs +62 -0
- package/src/rust/utils/spawn.rs +33 -0
- package/src/rust/utils/weak_scope.rs +0 -10
- package/src/rust/workspace.rs +846 -0
- package/src/svg/active-indicator.svg +3 -0
- package/src/themes/botanical.css +5 -2
- package/src/themes/defaults.css +54 -2
- package/src/themes/dracula.css +6 -3
- package/src/themes/gruvbox-dark.css +5 -2
- package/src/themes/gruvbox.css +5 -2
- package/src/themes/icons.css +4 -2
- package/src/themes/monokai.css +5 -2
- package/src/themes/phosphor.css +5 -2
- package/src/themes/pro-dark.css +5 -2
- package/src/themes/pro.css +2 -1
- package/src/themes/solarized-dark.css +5 -2
- package/src/themes/solarized.css +5 -2
- package/src/themes/vaporwave.css +5 -2
- package/src/ts/bootstrap.ts +8 -7
- package/src/ts/extensions.ts +30 -2
- package/src/ts/perspective-viewer.ts +10 -0
- package/src/ts/plugin.ts +72 -5
- package/src/ts/ts-rs/ClientOptions.ts +7 -0
- package/src/ts/ts-rs/ExportMethod.ts +3 -0
- package/src/ts/ts-rs/ExportOptions.ts +8 -0
- package/src/ts/ts-rs/GetClientOptions.ts +7 -0
- package/src/ts/ts-rs/GetTableOptions.ts +7 -0
- package/src/ts/ts-rs/Layout.ts +9 -0
- package/src/ts/ts-rs/Orientation.ts +6 -0
- package/src/ts/ts-rs/PanelOptions.ts +7 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
- package/src/ts/ts-rs/SplitLayout.ts +5 -0
- package/src/ts/ts-rs/TabLayout.ts +11 -0
- package/src/ts/ts-rs/ViewerConfig.ts +6 -2
- package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
- package/src/rust/tasks/intersection_observer.rs +0 -98
- package/src/rust/tasks/update_and_render.rs +0 -110
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
|
@@ -29,8 +29,10 @@ pub struct SplitPanelProps {
|
|
|
29
29
|
#[prop_or_default]
|
|
30
30
|
pub orientation: Orientation,
|
|
31
31
|
|
|
32
|
-
/// Whether to
|
|
33
|
-
///
|
|
32
|
+
/// Whether to OMIT children that render nothing (an empty fragment, or
|
|
33
|
+
/// any nesting of empty fragments — see [`is_empty_html`]) instead of
|
|
34
|
+
/// rendering them as empty panes. A skipped pane takes its divider with
|
|
35
|
+
/// it.
|
|
34
36
|
#[prop_or_default]
|
|
35
37
|
pub skip_empty: bool,
|
|
36
38
|
|
|
@@ -53,6 +55,50 @@ pub struct SplitPanelProps {
|
|
|
53
55
|
|
|
54
56
|
#[prop_or_default]
|
|
55
57
|
pub initial_size: Option<i32>,
|
|
58
|
+
|
|
59
|
+
/// When `true`, a divider drag does NOT apply pane sizes directly:
|
|
60
|
+
/// `MoveResizing` only emits `on_resize` with the proposed dims, and pane
|
|
61
|
+
/// 0's committed size is driven exclusively by the controlled
|
|
62
|
+
/// [`Self::size`] prop — set by the parent when it is ready (e.g. after
|
|
63
|
+
/// pre-rendering panel content at the target size). This is the presize
|
|
64
|
+
/// gate for continuous divider drags (`PRESIZE_EVERYWHERE_PLAN.md`,
|
|
65
|
+
/// P1): geometry never outruns content. Only meaningful when the
|
|
66
|
+
/// resizable pane is child 0 (as in the viewer's `app_panel`).
|
|
67
|
+
#[prop_or_default]
|
|
68
|
+
pub deferred: bool,
|
|
69
|
+
|
|
70
|
+
/// Controlled committed size for pane 0 (deferred mode). Unlike
|
|
71
|
+
/// [`Self::initial_size`] (read once at `create`), changes to this prop
|
|
72
|
+
/// re-derive pane 0's style on every `changed`.
|
|
73
|
+
#[prop_or_default]
|
|
74
|
+
pub size: Option<i32>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// `true` when `node` renders NOTHING: an empty fragment, or a fragment of
|
|
78
|
+
/// nothing but empty fragments. SEMANTIC, not literal — the `html!`
|
|
79
|
+
/// if-syntax (and other wrappers) nests an empty branch inside another
|
|
80
|
+
/// `VList` rather than yielding a bare `<></>`, which is why a literal
|
|
81
|
+
/// `x != html! { <></> }` comparison is not a usable emptiness test (see
|
|
82
|
+
/// the mechanism unit test). Deliberately conservative: `VText("")` and
|
|
83
|
+
/// other node kinds are NOT considered empty.
|
|
84
|
+
fn is_empty_html(node: &Html) -> bool {
|
|
85
|
+
match node {
|
|
86
|
+
Html::VList(list) => list.iter().all(is_empty_html),
|
|
87
|
+
_ => false,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The fixed-size pane style for a committed size (the complement pane
|
|
92
|
+
/// flex-fills).
|
|
93
|
+
fn size_style(orientation: Orientation, x: i32) -> String {
|
|
94
|
+
match orientation {
|
|
95
|
+
Orientation::Horizontal => {
|
|
96
|
+
format!("max-width:{x}px;min-width:{x}px;width:{x}px")
|
|
97
|
+
},
|
|
98
|
+
Orientation::Vertical => {
|
|
99
|
+
format!("max-height:{x}px;min-height:{x}px;height:{x}px")
|
|
100
|
+
},
|
|
101
|
+
}
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
impl SplitPanelProps {
|
|
@@ -67,6 +113,8 @@ impl PartialEq for SplitPanelProps {
|
|
|
67
113
|
&& self.children == other.children
|
|
68
114
|
&& self.orientation == other.orientation
|
|
69
115
|
&& self.reverse == other.reverse
|
|
116
|
+
&& self.size == other.size
|
|
117
|
+
&& self.deferred == other.deferred
|
|
70
118
|
}
|
|
71
119
|
}
|
|
72
120
|
|
|
@@ -111,15 +159,8 @@ impl Component for SplitPanel {
|
|
|
111
159
|
let refs = Vec::from_iter(std::iter::repeat_with(Default::default).take(len));
|
|
112
160
|
|
|
113
161
|
let mut styles = vec![Default::default(); len];
|
|
114
|
-
if let Some(x) =
|
|
115
|
-
styles[0] = Some(
|
|
116
|
-
Orientation::Horizontal => {
|
|
117
|
-
format!("max-width:{x}px;min-width:{x}px;width:{x}px")
|
|
118
|
-
},
|
|
119
|
-
Orientation::Vertical => {
|
|
120
|
-
format!("max-height:{x}px;min-height:{x}px;height:{x}px")
|
|
121
|
-
},
|
|
122
|
-
});
|
|
162
|
+
if let Some(x) = ctx.props().size.or(ctx.props().initial_size) {
|
|
163
|
+
styles[0] = Some(size_style(ctx.props().orientation, x));
|
|
123
164
|
}
|
|
124
165
|
|
|
125
166
|
Self {
|
|
@@ -155,7 +196,12 @@ impl Component for SplitPanel {
|
|
|
155
196
|
cb.emit(state.get_dimensions(client_offset));
|
|
156
197
|
}
|
|
157
198
|
|
|
158
|
-
|
|
199
|
+
// Deferred mode: propose only — the committed size arrives
|
|
200
|
+
// back through the controlled `size` prop once the parent
|
|
201
|
+
// has pre-rendered content at the target.
|
|
202
|
+
if !ctx.props().deferred {
|
|
203
|
+
self.styles[state.index] = state.get_style(client_offset);
|
|
204
|
+
}
|
|
159
205
|
}
|
|
160
206
|
},
|
|
161
207
|
};
|
|
@@ -173,19 +219,91 @@ impl Component for SplitPanel {
|
|
|
173
219
|
let new_len = ctx.props().children.len();
|
|
174
220
|
self.refs.resize_with(new_len, Default::default);
|
|
175
221
|
self.styles.resize(new_len, Default::default());
|
|
222
|
+
|
|
223
|
+
if let Some(state) = self.resize_state.as_ref() {
|
|
224
|
+
let skip_empty = ctx.props().skip_empty;
|
|
225
|
+
let still_visible = ctx
|
|
226
|
+
.props()
|
|
227
|
+
.children
|
|
228
|
+
.iter()
|
|
229
|
+
.enumerate()
|
|
230
|
+
.any(|(i, x)| i == state.index && (!skip_empty || !is_empty_html(&x)));
|
|
231
|
+
|
|
232
|
+
if !still_visible {
|
|
233
|
+
self.resize_state = None;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Deferred mode: pane 0's style tracks the controlled `size` prop
|
|
238
|
+
// (`None` = natural width, e.g. after a divider double-click reset).
|
|
239
|
+
if ctx.props().deferred {
|
|
240
|
+
self.styles[0] = ctx
|
|
241
|
+
.props()
|
|
242
|
+
.size
|
|
243
|
+
.map(|x| size_style(ctx.props().orientation, x));
|
|
244
|
+
}
|
|
245
|
+
|
|
176
246
|
true
|
|
177
247
|
}
|
|
178
248
|
|
|
179
249
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
180
|
-
let
|
|
250
|
+
let skip_empty = ctx.props().skip_empty;
|
|
251
|
+
let orientation = ctx.props().orientation;
|
|
252
|
+
|
|
253
|
+
// Pair each *visible* pane with its ORIGINAL child index and key every
|
|
254
|
+
// pane (and divider) by that stable identity — not its post-filter
|
|
255
|
+
// position. Toggling one pane in/out (e.g. the settings sidebar) then
|
|
256
|
+
// never shifts a sibling's key, so Yew reconciles the survivor in place
|
|
257
|
+
// rather than remounting it. A `MainPanel` remount here would tear down
|
|
258
|
+
// and recreate the embedded `<regular-layout>` and the `<slot>`s
|
|
259
|
+
// projecting the plugins — the bug this avoids. Dividers are independent
|
|
260
|
+
// keyed nodes for the same reason: a divider appearing before a pane must
|
|
261
|
+
// not alter that pane's own keyed subtree.
|
|
262
|
+
let panes = ctx
|
|
181
263
|
.props()
|
|
182
264
|
.children
|
|
183
265
|
.iter()
|
|
184
|
-
.filter(|x| !ctx.props().skip_empty || x != &html! { <></> })
|
|
185
266
|
.enumerate()
|
|
267
|
+
.filter(|(_, x)| !skip_empty || !is_empty_html(x))
|
|
186
268
|
.collect::<Vec<_>>();
|
|
187
269
|
|
|
188
|
-
let
|
|
270
|
+
let last = panes.len().saturating_sub(1);
|
|
271
|
+
let mut nodes: Vec<Html> = Vec::with_capacity(panes.len() * 2);
|
|
272
|
+
let mut prev: Option<usize> = None;
|
|
273
|
+
for (pos, (i, x)) in panes.into_iter().enumerate() {
|
|
274
|
+
// A divider precedes every visible pane except the first; it resizes
|
|
275
|
+
// the *previous* visible pane (`prev`, an original index).
|
|
276
|
+
if let Some(p) = prev {
|
|
277
|
+
nodes.push(html! {
|
|
278
|
+
<SplitPanelDivider
|
|
279
|
+
key={format!("divider-{i}")}
|
|
280
|
+
i={p}
|
|
281
|
+
{orientation}
|
|
282
|
+
link={ctx.link().clone()}
|
|
283
|
+
/>
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// The last visible pane flex-fills (rendered bare, no width
|
|
288
|
+
// override); earlier panes are `SplitPanelChild`s that can carry a
|
|
289
|
+
// dragged size.
|
|
290
|
+
nodes.push(if pos == last {
|
|
291
|
+
html! { <key={i}>{ x }</> }
|
|
292
|
+
} else {
|
|
293
|
+
html! {
|
|
294
|
+
<SplitPanelChild
|
|
295
|
+
key={i}
|
|
296
|
+
style={self.styles[i].clone()}
|
|
297
|
+
ref_={self.refs[i].clone()}
|
|
298
|
+
>
|
|
299
|
+
{ x }
|
|
300
|
+
</SplitPanelChild>
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
prev = Some(i);
|
|
305
|
+
}
|
|
306
|
+
|
|
189
307
|
let mut classes = classes!("split-panel");
|
|
190
308
|
if orientation == Orientation::Vertical {
|
|
191
309
|
classes.push("orient-vertical");
|
|
@@ -195,53 +313,7 @@ impl Component for SplitPanel {
|
|
|
195
313
|
classes.push("orient-reverse");
|
|
196
314
|
}
|
|
197
315
|
|
|
198
|
-
let
|
|
199
|
-
let contents = html! {
|
|
200
|
-
<>
|
|
201
|
-
for (i, x) in iter {
|
|
202
|
-
if i == 0 {
|
|
203
|
-
if count == 1 {
|
|
204
|
-
<key=0>
|
|
205
|
-
{x}
|
|
206
|
-
</>
|
|
207
|
-
} else {
|
|
208
|
-
<SplitPanelChild
|
|
209
|
-
key=0
|
|
210
|
-
style={self.styles[i].clone()}
|
|
211
|
-
ref_={self.refs[i].clone()}
|
|
212
|
-
>
|
|
213
|
-
{ x }
|
|
214
|
-
</SplitPanelChild>
|
|
215
|
-
}
|
|
216
|
-
} else if i == count - 1 {
|
|
217
|
-
<key={i}>
|
|
218
|
-
<SplitPanelDivider
|
|
219
|
-
i={i - 1}
|
|
220
|
-
orientation={ctx.props().orientation}
|
|
221
|
-
link={ctx.link().clone()}
|
|
222
|
-
/>
|
|
223
|
-
{ x }
|
|
224
|
-
</>
|
|
225
|
-
} else {
|
|
226
|
-
<key={i}>
|
|
227
|
-
<SplitPanelDivider
|
|
228
|
-
i={i - 1}
|
|
229
|
-
orientation={ctx.props().orientation}
|
|
230
|
-
link={ctx.link().clone()}
|
|
231
|
-
/>
|
|
232
|
-
<SplitPanelChild
|
|
233
|
-
style={self.styles[i].clone()}
|
|
234
|
-
ref_={self.refs[i].clone()}
|
|
235
|
-
>
|
|
236
|
-
{ x }
|
|
237
|
-
</SplitPanelChild>
|
|
238
|
-
</>
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
</>
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
// TODO consider removing this
|
|
316
|
+
let contents = html! { <>{ for nodes.into_iter() }</> };
|
|
245
317
|
if ctx.props().no_wrap {
|
|
246
318
|
html! { { contents } }
|
|
247
319
|
} else {
|
|
@@ -490,3 +562,44 @@ impl ResizingState {
|
|
|
490
562
|
Ok(self.body_style.set_property("cursor", &self.cursor)?)
|
|
491
563
|
}
|
|
492
564
|
}
|
|
565
|
+
|
|
566
|
+
#[cfg(test)]
|
|
567
|
+
mod tests {
|
|
568
|
+
use super::*;
|
|
569
|
+
|
|
570
|
+
#[test]
|
|
571
|
+
fn html_if_empty_branch_is_not_literally_empty() {
|
|
572
|
+
let via_if: Html = html! { if false { <div /> } else { <></> } };
|
|
573
|
+
assert_ne!(via_if, html! { <></> });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#[test]
|
|
577
|
+
fn empty_fragment_is_empty() {
|
|
578
|
+
assert!(is_empty_html(&html! { <></> }));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
#[test]
|
|
582
|
+
fn html_if_empty_branch_is_semantically_empty() {
|
|
583
|
+
assert!(is_empty_html(&html! { if false { <div/> } else { <></> } }));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#[test]
|
|
587
|
+
fn nested_empty_fragments_are_empty() {
|
|
588
|
+
assert!(is_empty_html(&html! { <><><></></></> }));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
#[test]
|
|
592
|
+
fn tag_is_not_empty() {
|
|
593
|
+
assert!(!is_empty_html(&html! { <div/> }));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
#[test]
|
|
597
|
+
fn fragment_containing_a_tag_is_not_empty() {
|
|
598
|
+
assert!(!is_empty_html(&html! { <><div/></> }));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[test]
|
|
602
|
+
fn text_is_not_empty() {
|
|
603
|
+
assert!(!is_empty_html(&html! { { "" } }));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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 web_sys::MouseEvent;
|
|
14
|
+
use yew::prelude::*;
|
|
15
|
+
|
|
16
|
+
/// A single command row in a [`ContextMenu`].
|
|
17
|
+
#[derive(Clone, PartialEq)]
|
|
18
|
+
pub struct ContextMenuItem {
|
|
19
|
+
pub label: String,
|
|
20
|
+
pub on_select: Callback<()>,
|
|
21
|
+
pub disabled: bool,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// A row in a [`ContextMenu`]: a plain command [`ContextMenuItem`], a
|
|
25
|
+
/// non-interactive group label, or a row with a hover-opened sub-menu flyout.
|
|
26
|
+
#[derive(Clone, PartialEq)]
|
|
27
|
+
pub enum ContextMenuEntry {
|
|
28
|
+
Item(ContextMenuItem),
|
|
29
|
+
Header(String),
|
|
30
|
+
Submenu {
|
|
31
|
+
label: String,
|
|
32
|
+
|
|
33
|
+
/// Click-through command for the parent row itself (`None` makes the
|
|
34
|
+
/// row hover-only).
|
|
35
|
+
on_select: Option<Callback<()>>,
|
|
36
|
+
entries: Vec<ContextMenuEntry>,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Properties, PartialEq)]
|
|
41
|
+
pub struct ContextMenuProps {
|
|
42
|
+
pub entries: Vec<ContextMenuEntry>,
|
|
43
|
+
|
|
44
|
+
/// Fired after an item is selected. Outside-click dismissal is the
|
|
45
|
+
/// enclosing `PortalModal`'s job (blur), not this component's.
|
|
46
|
+
#[prop_or_default]
|
|
47
|
+
pub on_close: Callback<()>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// The command-item list of a panel context menu. Purely presentational — the
|
|
51
|
+
/// command set is supplied by the parent as plain [`Callback`]s, and
|
|
52
|
+
/// positioning/dismissal/theming belong to the enclosing `PortalModal` (the
|
|
53
|
+
/// same body-mounted, `theme`-attributed delivery as the Export/Copy menus).
|
|
54
|
+
/// Sub-menus are CSS-only `:hover` flyouts nested inside their parent row, so
|
|
55
|
+
/// they live inside the same `PortalModal` host (focus never leaves it) and
|
|
56
|
+
/// selection routes through the same `on_select` + `on_close` path.
|
|
57
|
+
pub struct ContextMenu;
|
|
58
|
+
|
|
59
|
+
impl Component for ContextMenu {
|
|
60
|
+
type Message = ();
|
|
61
|
+
type Properties = ContextMenuProps;
|
|
62
|
+
|
|
63
|
+
fn create(_ctx: &Context<Self>) -> Self {
|
|
64
|
+
Self
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
68
|
+
let props = ctx.props();
|
|
69
|
+
let entries = props
|
|
70
|
+
.entries
|
|
71
|
+
.iter()
|
|
72
|
+
.map(|entry| render_entry(entry, &props.on_close));
|
|
73
|
+
|
|
74
|
+
html! { <div class="context-menu">{ for entries }</div> }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn render_entry(entry: &ContextMenuEntry, on_close: &Callback<()>) -> Html {
|
|
79
|
+
match entry {
|
|
80
|
+
ContextMenuEntry::Item(item) => {
|
|
81
|
+
let disabled = item.disabled;
|
|
82
|
+
let onclick = {
|
|
83
|
+
let on_select = item.on_select.clone();
|
|
84
|
+
let on_close = on_close.clone();
|
|
85
|
+
Callback::from(move |e: MouseEvent| {
|
|
86
|
+
e.stop_propagation();
|
|
87
|
+
if !disabled {
|
|
88
|
+
on_select.emit(());
|
|
89
|
+
on_close.emit(());
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let mut class = classes!("context-menu-item");
|
|
95
|
+
if disabled {
|
|
96
|
+
class.push("disabled");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
html! { <span {class} {onclick}>{ item.label.clone() }</span> }
|
|
100
|
+
},
|
|
101
|
+
ContextMenuEntry::Header(label) => {
|
|
102
|
+
html! { <span class="context-menu-header">{ label.clone() }</span> }
|
|
103
|
+
},
|
|
104
|
+
ContextMenuEntry::Submenu {
|
|
105
|
+
label,
|
|
106
|
+
on_select,
|
|
107
|
+
entries,
|
|
108
|
+
} => {
|
|
109
|
+
let onclick = {
|
|
110
|
+
let on_select = on_select.clone();
|
|
111
|
+
let on_close = on_close.clone();
|
|
112
|
+
Callback::from(move |e: MouseEvent| {
|
|
113
|
+
e.stop_propagation();
|
|
114
|
+
if let Some(on_select) = &on_select {
|
|
115
|
+
on_select.emit(());
|
|
116
|
+
on_close.emit(());
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let children = entries.iter().map(|entry| render_entry(entry, on_close));
|
|
122
|
+
html! {
|
|
123
|
+
<span class="context-menu-item has-submenu" {onclick}>
|
|
124
|
+
{ label.clone() }
|
|
125
|
+
<div class="context-menu context-menu-submenu">{ for children }</div>
|
|
126
|
+
</span>
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -240,6 +240,9 @@ impl DebugPanelProps {
|
|
|
240
240
|
&props.session,
|
|
241
241
|
&props.renderer,
|
|
242
242
|
&props.presentation,
|
|
243
|
+
// A user-pasted config apply — an explicit request,
|
|
244
|
+
// same affordance as the public `restore()`.
|
|
245
|
+
crate::tasks::RunOrigin::Public,
|
|
243
246
|
config,
|
|
244
247
|
async { Ok(()) },
|
|
245
248
|
)
|
|
@@ -0,0 +1,76 @@
|
|
|
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 perspective_client::config::Filter;
|
|
14
|
+
use web_sys::MouseEvent;
|
|
15
|
+
use yew::prelude::*;
|
|
16
|
+
|
|
17
|
+
#[derive(Properties, PartialEq)]
|
|
18
|
+
pub struct GlobalFilterBarProps {
|
|
19
|
+
/// The element-level global filters (applied to every non-master panel).
|
|
20
|
+
pub filters: Vec<Filter>,
|
|
21
|
+
|
|
22
|
+
/// Fired with a filter's index to remove it.
|
|
23
|
+
pub on_remove: Callback<usize>,
|
|
24
|
+
|
|
25
|
+
/// Fired to clear all global filters.
|
|
26
|
+
pub on_clear: Callback<()>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// The global-filter bar: the element-level filters (fed by master/detail
|
|
30
|
+
/// selection) as removable chips, rendered inline in the `StatusBar` between
|
|
31
|
+
/// the row stats and the menu icons. Purely presentational — the parent owns
|
|
32
|
+
/// the filter set and applies it.
|
|
33
|
+
pub struct GlobalFilterBar;
|
|
34
|
+
|
|
35
|
+
impl Component for GlobalFilterBar {
|
|
36
|
+
type Message = ();
|
|
37
|
+
type Properties = GlobalFilterBarProps;
|
|
38
|
+
|
|
39
|
+
fn create(_ctx: &Context<Self>) -> Self {
|
|
40
|
+
Self
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
44
|
+
let chips = ctx
|
|
45
|
+
.props()
|
|
46
|
+
.filters
|
|
47
|
+
.iter()
|
|
48
|
+
.enumerate()
|
|
49
|
+
.map(|(index, filter)| {
|
|
50
|
+
let on_remove = ctx.props().on_remove.clone();
|
|
51
|
+
let onclick = Callback::from(move |e: MouseEvent| {
|
|
52
|
+
e.stop_propagation();
|
|
53
|
+
on_remove.emit(index);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let label = format!("{} {} {}", filter.column(), filter.op(), filter.term());
|
|
57
|
+
html! {
|
|
58
|
+
<span class="global-filter-chip">
|
|
59
|
+
<span class="global-filter-chip-label">{ label }</span>
|
|
60
|
+
<span class="global-filter-chip-remove" {onclick}>{ "×" }</span>
|
|
61
|
+
</span>
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let on_clear = ctx.props().on_clear.clone();
|
|
66
|
+
let onclear = Callback::from(move |_: MouseEvent| on_clear.emit(()));
|
|
67
|
+
|
|
68
|
+
html! {
|
|
69
|
+
<div id="global_filter_bar">
|
|
70
|
+
<span class="global-filter-bar-label">{ "Filter" }</span>
|
|
71
|
+
<span class="global-filter-bar-chips">{ for chips }</span>
|
|
72
|
+
<span class="global-filter-bar-clear" onclick={onclear}>{ "Clear" }</span>
|
|
73
|
+
</div>
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|