@perspective-dev/viewer 4.5.2 → 5.1.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/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.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 +41 -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 +8 -2
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
- package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- 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 +299 -78
- package/dist/wasm/perspective-viewer.js +544 -186
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
- package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_context_menu.css +14 -0
- package/src/css/column-selector.css +1 -0
- package/src/css/column-settings-panel.css +68 -3
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +22 -1
- package/src/css/containers/context-menu.css +107 -0
- package/src/css/containers/tabs.css +2 -2
- 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 +188 -70
- package/src/rust/components/column_dropdown.rs +40 -30
- package/src/rust/components/column_selector/active_column.rs +28 -45
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +40 -82
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/filter_column.rs +5 -15
- package/src/rust/components/column_selector/inactive_column.rs +11 -9
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +25 -14
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +73 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +181 -62
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/editable_header.rs +2 -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 +316 -71
- package/src/rust/components/mod.rs +5 -0
- package/src/rust/components/panel_menu.rs +493 -0
- package/src/rust/components/panel_tab.rs +510 -0
- package/src/rust/components/plugin_tab.rs +1 -1
- package/src/rust/components/portal.rs +7 -0
- 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 +182 -0
- package/src/rust/components/viewer/panels.rs +326 -0
- package/src/rust/components/viewer/render.rs +255 -0
- package/src/rust/components/viewer/settings.rs +434 -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 +245 -756
- package/src/rust/components/window_editor.rs +1013 -0
- 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 +24 -2
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +66 -33
- package/src/rust/queries/columns_iter_set.rs +7 -1
- 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 +463 -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 +384 -838
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +66 -9
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +755 -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/edit_window.rs +155 -0
- package/src/rust/tasks/eject.rs +64 -23
- package/src/rust/tasks/mod.rs +30 -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/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/completion.rs +56 -0
- package/src/rust/utils/custom_element.rs +5 -0
- package/src/rust/utils/debounce.rs +189 -17
- 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/svg/pin-icon.svg +5 -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 +5 -2
- package/src/themes/intl/de.css +7 -0
- package/src/themes/intl/es.css +7 -0
- package/src/themes/intl/fr.css +7 -0
- package/src/themes/intl/ja.css +7 -0
- package/src/themes/intl/pt.css +7 -0
- package/src/themes/intl/zh.css +7 -0
- package/src/themes/intl.css +10 -1
- 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 +16 -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 +7 -2
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
- package/src/ts/ts-rs/WindowAggregate.ts +3 -0
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +46 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- 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-8a8c7d5d696b43ae/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/inline5.js} +0 -0
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
use perspective_client::clone;
|
|
14
|
-
use
|
|
14
|
+
use wasm_bindgen::JsCast;
|
|
15
|
+
use wasm_bindgen::prelude::Closure;
|
|
16
|
+
use web_sys::HtmlElement;
|
|
15
17
|
use yew::{
|
|
16
|
-
Callback, Children, Html, Properties, function_component, html, use_effect_with,
|
|
17
|
-
|
|
18
|
+
Callback, Children, Html, Properties, function_component, html, use_effect_with, use_mut_ref,
|
|
19
|
+
use_node_ref,
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
use crate::components::containers::sidebar_close_button::SidebarCloseButton;
|
|
21
23
|
use crate::components::editable_header::{EditableHeader, EditableHeaderProps};
|
|
24
|
+
use crate::js::{ResizeObserver, ResizeObserverEntry};
|
|
22
25
|
|
|
23
26
|
#[derive(PartialEq, Clone, Properties)]
|
|
24
27
|
pub struct SidebarProps {
|
|
@@ -31,6 +34,28 @@ pub struct SidebarProps {
|
|
|
31
34
|
pub width_override: Option<i32>,
|
|
32
35
|
pub selected_tab: Option<usize>,
|
|
33
36
|
pub header_props: EditableHeaderProps,
|
|
37
|
+
|
|
38
|
+
/// Trap-door width shared across this sidebar's tabs: the lifted
|
|
39
|
+
/// running max of the widths this component reports through
|
|
40
|
+
/// `on_auto_width`. Held by the parent (ultimately
|
|
41
|
+
/// `PerspectiveViewer`'s geometry state, like the settings panel's
|
|
42
|
+
/// Query/Plugin/Debug trap-door) so it survives tab switches AND
|
|
43
|
+
/// sidebar re-mounts, and clears on divider reset.
|
|
44
|
+
#[prop_or_default]
|
|
45
|
+
pub auto_width: f64,
|
|
46
|
+
|
|
47
|
+
/// Fires with the sidebar's rendered width after each render; the
|
|
48
|
+
/// owner keeps the running max threaded back as `auto_width`.
|
|
49
|
+
#[prop_or_default]
|
|
50
|
+
pub on_auto_width: Callback<f64>,
|
|
51
|
+
|
|
52
|
+
/// Pinned state for the header's pin toggle; the button renders only
|
|
53
|
+
/// when `on_toggle_pin` is provided.
|
|
54
|
+
#[prop_or_default]
|
|
55
|
+
pub is_pinned: bool,
|
|
56
|
+
|
|
57
|
+
#[prop_or_default]
|
|
58
|
+
pub on_toggle_pin: Option<Callback<()>>,
|
|
34
59
|
}
|
|
35
60
|
|
|
36
61
|
/// Sidebars are designed to live in a
|
|
@@ -39,33 +64,85 @@ pub struct SidebarProps {
|
|
|
39
64
|
pub fn Sidebar(p: &SidebarProps) -> Html {
|
|
40
65
|
let id = &p.id_prefix;
|
|
41
66
|
let noderef = use_node_ref();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
|
|
68
|
+
// The trap-door reports the sidebar's rendered width to its owner via
|
|
69
|
+
// a `ResizeObserver` on the sidebar element, NOT a render effect:
|
|
70
|
+
// width changes are driven by DOM mutations anywhere in the tab
|
|
71
|
+
// subtree (e.g. the window editor staging a long column name into a
|
|
72
|
+
// slot), which need not re-render this component at all. The observer
|
|
73
|
+
// sees every one. `contentRect` (not the border box) is load-bearing:
|
|
74
|
+
// the sizer below is a content child, so ratcheting the border box
|
|
75
|
+
// would feed any sidebar padding back into unbounded growth. A manual
|
|
76
|
+
// divider drag (`width_override`) disables the ratchet until the
|
|
77
|
+
// divider resets.
|
|
78
|
+
let live_props = use_mut_ref(|| (Callback::<f64>::default(), None::<i32>));
|
|
79
|
+
*live_props.borrow_mut() = (p.on_auto_width.clone(), p.width_override);
|
|
80
|
+
use_effect_with((), {
|
|
81
|
+
clone!(noderef, live_props);
|
|
82
|
+
move |_| {
|
|
83
|
+
let closure =
|
|
84
|
+
Closure::<dyn FnMut(js_sys::Array)>::new(move |entries: js_sys::Array| {
|
|
85
|
+
let (on_auto_width, width_override) = live_props.borrow().clone();
|
|
86
|
+
if width_override.is_none() {
|
|
87
|
+
for entry in entries.iter() {
|
|
88
|
+
let entry: ResizeObserverEntry = entry.unchecked_into();
|
|
89
|
+
on_auto_width.emit(entry.content_rect().width());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let observer = ResizeObserver::new(closure.as_ref().unchecked_ref());
|
|
95
|
+
let elem = noderef.cast::<HtmlElement>();
|
|
96
|
+
if let Some(elem) = &elem {
|
|
97
|
+
observer.observe(elem);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
move || {
|
|
101
|
+
if let Some(elem) = &elem {
|
|
102
|
+
observer.unobserve(elem);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
drop(closure);
|
|
59
106
|
}
|
|
60
107
|
}
|
|
61
108
|
});
|
|
62
109
|
|
|
63
|
-
let
|
|
110
|
+
let auto_width = if p.width_override.is_none() {
|
|
111
|
+
p.auto_width
|
|
112
|
+
} else {
|
|
113
|
+
0.0
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let width_style = format!("min-width: 200px; width: {}px", auto_width);
|
|
117
|
+
let pin_button = p.on_toggle_pin.as_ref().map(|cb| {
|
|
118
|
+
let onclick = {
|
|
119
|
+
let cb = cb.clone();
|
|
120
|
+
Callback::from(move |_: web_sys::MouseEvent| cb.emit(()))
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let mut class = yew::classes!("sidebar_pin_button");
|
|
124
|
+
if p.is_pinned {
|
|
125
|
+
class.push("is-pinned");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
html! {
|
|
129
|
+
<span
|
|
130
|
+
id={format!("{id}_pin_button")}
|
|
131
|
+
{class}
|
|
132
|
+
title={if p.is_pinned { "Unpin" } else { "Pin" }}
|
|
133
|
+
{onclick}
|
|
134
|
+
/>
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
64
138
|
html! {
|
|
65
139
|
<>
|
|
66
140
|
<SidebarCloseButton id={format!("{id}_close_button")} on_close_sidebar={&p.on_close} />
|
|
67
141
|
<div class="sidebar_column" id={format!("{id}_sidebar")} ref={noderef}>
|
|
68
|
-
<div class="sidebar_header"
|
|
142
|
+
<div class="sidebar_header">
|
|
143
|
+
<EditableHeader ..p.header_props.clone() />
|
|
144
|
+
{ pin_button }
|
|
145
|
+
</div>
|
|
69
146
|
<div class="sidebar_border" id={format!("{id}_border")} />
|
|
70
147
|
{ p.children.iter().collect::<Html>() }
|
|
71
148
|
<div class="sidebar-auto-width" style={width_style} />
|
|
@@ -26,11 +26,17 @@ pub struct SplitPanelProps {
|
|
|
26
26
|
#[prop_or_default]
|
|
27
27
|
pub id: Option<String>,
|
|
28
28
|
|
|
29
|
+
/// Extra classes on the root element (ignored under `no_wrap`).
|
|
30
|
+
#[prop_or_default]
|
|
31
|
+
pub class: Classes,
|
|
32
|
+
|
|
29
33
|
#[prop_or_default]
|
|
30
34
|
pub orientation: Orientation,
|
|
31
35
|
|
|
32
|
-
/// Whether to
|
|
33
|
-
///
|
|
36
|
+
/// Whether to OMIT children that render nothing (an empty fragment, or
|
|
37
|
+
/// any nesting of empty fragments — see [`is_empty_html`]) instead of
|
|
38
|
+
/// rendering them as empty panes. A skipped pane takes its divider with
|
|
39
|
+
/// it.
|
|
34
40
|
#[prop_or_default]
|
|
35
41
|
pub skip_empty: bool,
|
|
36
42
|
|
|
@@ -53,6 +59,50 @@ pub struct SplitPanelProps {
|
|
|
53
59
|
|
|
54
60
|
#[prop_or_default]
|
|
55
61
|
pub initial_size: Option<i32>,
|
|
62
|
+
|
|
63
|
+
/// When `true`, a divider drag does NOT apply pane sizes directly:
|
|
64
|
+
/// `MoveResizing` only emits `on_resize` with the proposed dims, and pane
|
|
65
|
+
/// 0's committed size is driven exclusively by the controlled
|
|
66
|
+
/// [`Self::size`] prop — set by the parent when it is ready (e.g. after
|
|
67
|
+
/// pre-rendering panel content at the target size). This is the presize
|
|
68
|
+
/// gate for continuous divider drags (`PRESIZE_EVERYWHERE_PLAN.md`,
|
|
69
|
+
/// P1): geometry never outruns content. Only meaningful when the
|
|
70
|
+
/// resizable pane is child 0 (as in the viewer's `app_panel`).
|
|
71
|
+
#[prop_or_default]
|
|
72
|
+
pub deferred: bool,
|
|
73
|
+
|
|
74
|
+
/// Controlled committed size for pane 0 (deferred mode). Unlike
|
|
75
|
+
/// [`Self::initial_size`] (read once at `create`), changes to this prop
|
|
76
|
+
/// re-derive pane 0's style on every `changed`.
|
|
77
|
+
#[prop_or_default]
|
|
78
|
+
pub size: Option<i32>,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// `true` when `node` renders NOTHING: an empty fragment, or a fragment of
|
|
82
|
+
/// nothing but empty fragments. SEMANTIC, not literal — the `html!`
|
|
83
|
+
/// if-syntax (and other wrappers) nests an empty branch inside another
|
|
84
|
+
/// `VList` rather than yielding a bare `<></>`, which is why a literal
|
|
85
|
+
/// `x != html! { <></> }` comparison is not a usable emptiness test (see
|
|
86
|
+
/// the mechanism unit test). Deliberately conservative: `VText("")` and
|
|
87
|
+
/// other node kinds are NOT considered empty.
|
|
88
|
+
fn is_empty_html(node: &Html) -> bool {
|
|
89
|
+
match node {
|
|
90
|
+
Html::VList(list) => list.iter().all(is_empty_html),
|
|
91
|
+
_ => false,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// The fixed-size pane style for a committed size (the complement pane
|
|
96
|
+
/// flex-fills).
|
|
97
|
+
fn size_style(orientation: Orientation, x: i32) -> String {
|
|
98
|
+
match orientation {
|
|
99
|
+
Orientation::Horizontal => {
|
|
100
|
+
format!("max-width:{x}px;min-width:{x}px;width:{x}px")
|
|
101
|
+
},
|
|
102
|
+
Orientation::Vertical => {
|
|
103
|
+
format!("max-height:{x}px;min-height:{x}px;height:{x}px")
|
|
104
|
+
},
|
|
105
|
+
}
|
|
56
106
|
}
|
|
57
107
|
|
|
58
108
|
impl SplitPanelProps {
|
|
@@ -64,9 +114,12 @@ impl SplitPanelProps {
|
|
|
64
114
|
impl PartialEq for SplitPanelProps {
|
|
65
115
|
fn eq(&self, other: &Self) -> bool {
|
|
66
116
|
self.id == other.id
|
|
117
|
+
&& self.class == other.class
|
|
67
118
|
&& self.children == other.children
|
|
68
119
|
&& self.orientation == other.orientation
|
|
69
120
|
&& self.reverse == other.reverse
|
|
121
|
+
&& self.size == other.size
|
|
122
|
+
&& self.deferred == other.deferred
|
|
70
123
|
}
|
|
71
124
|
}
|
|
72
125
|
|
|
@@ -111,15 +164,8 @@ impl Component for SplitPanel {
|
|
|
111
164
|
let refs = Vec::from_iter(std::iter::repeat_with(Default::default).take(len));
|
|
112
165
|
|
|
113
166
|
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
|
-
});
|
|
167
|
+
if let Some(x) = ctx.props().size.or(ctx.props().initial_size) {
|
|
168
|
+
styles[0] = Some(size_style(ctx.props().orientation, x));
|
|
123
169
|
}
|
|
124
170
|
|
|
125
171
|
Self {
|
|
@@ -155,7 +201,12 @@ impl Component for SplitPanel {
|
|
|
155
201
|
cb.emit(state.get_dimensions(client_offset));
|
|
156
202
|
}
|
|
157
203
|
|
|
158
|
-
|
|
204
|
+
// Deferred mode: propose only — the committed size arrives
|
|
205
|
+
// back through the controlled `size` prop once the parent
|
|
206
|
+
// has pre-rendered content at the target.
|
|
207
|
+
if !ctx.props().deferred {
|
|
208
|
+
self.styles[state.index] = state.get_style(client_offset);
|
|
209
|
+
}
|
|
159
210
|
}
|
|
160
211
|
},
|
|
161
212
|
};
|
|
@@ -173,20 +224,93 @@ impl Component for SplitPanel {
|
|
|
173
224
|
let new_len = ctx.props().children.len();
|
|
174
225
|
self.refs.resize_with(new_len, Default::default);
|
|
175
226
|
self.styles.resize(new_len, Default::default());
|
|
227
|
+
|
|
228
|
+
if let Some(state) = self.resize_state.as_ref() {
|
|
229
|
+
let skip_empty = ctx.props().skip_empty;
|
|
230
|
+
let still_visible = ctx
|
|
231
|
+
.props()
|
|
232
|
+
.children
|
|
233
|
+
.iter()
|
|
234
|
+
.enumerate()
|
|
235
|
+
.any(|(i, x)| i == state.index && (!skip_empty || !is_empty_html(&x)));
|
|
236
|
+
|
|
237
|
+
if !still_visible {
|
|
238
|
+
self.resize_state = None;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Deferred mode: pane 0's style tracks the controlled `size` prop
|
|
243
|
+
// (`None` = natural width, e.g. after a divider double-click reset).
|
|
244
|
+
if ctx.props().deferred {
|
|
245
|
+
self.styles[0] = ctx
|
|
246
|
+
.props()
|
|
247
|
+
.size
|
|
248
|
+
.map(|x| size_style(ctx.props().orientation, x));
|
|
249
|
+
}
|
|
250
|
+
|
|
176
251
|
true
|
|
177
252
|
}
|
|
178
253
|
|
|
179
254
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
180
|
-
let
|
|
255
|
+
let skip_empty = ctx.props().skip_empty;
|
|
256
|
+
let orientation = ctx.props().orientation;
|
|
257
|
+
|
|
258
|
+
// Pair each *visible* pane with its ORIGINAL child index and key every
|
|
259
|
+
// pane (and divider) by that stable identity — not its post-filter
|
|
260
|
+
// position. Toggling one pane in/out (e.g. the settings sidebar) then
|
|
261
|
+
// never shifts a sibling's key, so Yew reconciles the survivor in place
|
|
262
|
+
// rather than remounting it. A `MainPanel` remount here would tear down
|
|
263
|
+
// and recreate the embedded `<regular-layout>` and the `<slot>`s
|
|
264
|
+
// projecting the plugins — the bug this avoids. Dividers are independent
|
|
265
|
+
// keyed nodes for the same reason: a divider appearing before a pane must
|
|
266
|
+
// not alter that pane's own keyed subtree.
|
|
267
|
+
let panes = ctx
|
|
181
268
|
.props()
|
|
182
269
|
.children
|
|
183
270
|
.iter()
|
|
184
|
-
.filter(|x| !ctx.props().skip_empty || x != &html! { <></> })
|
|
185
271
|
.enumerate()
|
|
272
|
+
.filter(|(_, x)| !skip_empty || !is_empty_html(x))
|
|
186
273
|
.collect::<Vec<_>>();
|
|
187
274
|
|
|
188
|
-
let
|
|
275
|
+
let last = panes.len().saturating_sub(1);
|
|
276
|
+
let mut nodes: Vec<Html> = Vec::with_capacity(panes.len() * 2);
|
|
277
|
+
let mut prev: Option<usize> = None;
|
|
278
|
+
for (pos, (i, x)) in panes.into_iter().enumerate() {
|
|
279
|
+
// A divider precedes every visible pane except the first; it resizes
|
|
280
|
+
// the *previous* visible pane (`prev`, an original index).
|
|
281
|
+
if let Some(p) = prev {
|
|
282
|
+
nodes.push(html! {
|
|
283
|
+
<SplitPanelDivider
|
|
284
|
+
key={format!("divider-{i}")}
|
|
285
|
+
i={p}
|
|
286
|
+
{orientation}
|
|
287
|
+
link={ctx.link().clone()}
|
|
288
|
+
/>
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The last visible pane flex-fills (rendered bare, no width
|
|
293
|
+
// override); earlier panes are `SplitPanelChild`s that can carry a
|
|
294
|
+
// dragged size.
|
|
295
|
+
nodes.push(if pos == last {
|
|
296
|
+
html! { <key={i}>{ x }</> }
|
|
297
|
+
} else {
|
|
298
|
+
html! {
|
|
299
|
+
<SplitPanelChild
|
|
300
|
+
key={i}
|
|
301
|
+
style={self.styles[i].clone()}
|
|
302
|
+
ref_={self.refs[i].clone()}
|
|
303
|
+
>
|
|
304
|
+
{ x }
|
|
305
|
+
</SplitPanelChild>
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
prev = Some(i);
|
|
310
|
+
}
|
|
311
|
+
|
|
189
312
|
let mut classes = classes!("split-panel");
|
|
313
|
+
classes.extend(ctx.props().class.clone());
|
|
190
314
|
if orientation == Orientation::Vertical {
|
|
191
315
|
classes.push("orient-vertical");
|
|
192
316
|
}
|
|
@@ -195,53 +319,7 @@ impl Component for SplitPanel {
|
|
|
195
319
|
classes.push("orient-reverse");
|
|
196
320
|
}
|
|
197
321
|
|
|
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
|
|
322
|
+
let contents = html! { <>{ for nodes.into_iter() }</> };
|
|
245
323
|
if ctx.props().no_wrap {
|
|
246
324
|
html! { { contents } }
|
|
247
325
|
} else {
|
|
@@ -490,3 +568,44 @@ impl ResizingState {
|
|
|
490
568
|
Ok(self.body_style.set_property("cursor", &self.cursor)?)
|
|
491
569
|
}
|
|
492
570
|
}
|
|
571
|
+
|
|
572
|
+
#[cfg(test)]
|
|
573
|
+
mod tests {
|
|
574
|
+
use super::*;
|
|
575
|
+
|
|
576
|
+
#[test]
|
|
577
|
+
fn html_if_empty_branch_is_not_literally_empty() {
|
|
578
|
+
let via_if: Html = html! { if false { <div /> } else { <></> } };
|
|
579
|
+
assert_ne!(via_if, html! { <></> });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#[test]
|
|
583
|
+
fn empty_fragment_is_empty() {
|
|
584
|
+
assert!(is_empty_html(&html! { <></> }));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#[test]
|
|
588
|
+
fn html_if_empty_branch_is_semantically_empty() {
|
|
589
|
+
assert!(is_empty_html(&html! { if false { <div/> } else { <></> } }));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[test]
|
|
593
|
+
fn nested_empty_fragments_are_empty() {
|
|
594
|
+
assert!(is_empty_html(&html! { <><><></></></> }));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
#[test]
|
|
598
|
+
fn tag_is_not_empty() {
|
|
599
|
+
assert!(!is_empty_html(&html! { <div/> }));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#[test]
|
|
603
|
+
fn fragment_containing_a_tag_is_not_empty() {
|
|
604
|
+
assert!(!is_empty_html(&html! { <><div/></> }));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#[test]
|
|
608
|
+
fn text_is_not_empty() {
|
|
609
|
+
assert!(!is_empty_html(&html! { { "" } }));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -119,10 +119,12 @@ impl Component for EditableHeader {
|
|
|
119
119
|
}
|
|
120
120
|
let metadata = &ctx.props().metadata;
|
|
121
121
|
let expressions = metadata.get_expression_columns();
|
|
122
|
+
let windows = metadata.get_window_columns();
|
|
122
123
|
let found = metadata
|
|
123
124
|
.get_table_columns()?
|
|
124
125
|
.iter()
|
|
125
126
|
.chain(expressions)
|
|
127
|
+
.chain(windows)
|
|
126
128
|
.contains(&new_value);
|
|
127
129
|
Some(!found)
|
|
128
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
|
)
|