@perspective-dev/viewer 4.2.0 → 4.4.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 +2 -2
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -0
- 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/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 +32 -1
- package/dist/esm/perspective-viewer.d.ts +1 -0
- package/dist/esm/perspective-viewer.inline.js +2 -2
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +2 -2
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/ts-rs/GroupRollupMode.d.ts +1 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +2 -0
- package/dist/wasm/perspective-viewer.d.ts +57 -53
- package/dist/wasm/perspective-viewer.js +197 -164
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +17 -18
- package/package.json +9 -6
- package/src/{less/aggregate-selector.less → css/aggregate-selector.css} +23 -20
- package/src/css/column-dropdown.css +109 -0
- package/src/{less/column-selector.less → css/column-selector.css} +161 -159
- package/src/{less/column-settings-panel.less → css/column-settings-panel.css} +69 -59
- package/src/{less/column-style.less → css/column-style.css} +52 -66
- package/src/{less/column-symbol-attributes.less → css/column-symbol-attributes.css} +15 -14
- package/src/css/config-selector.css +441 -0
- package/src/{less/containers/dropdown-menu.less → css/containers/dropdown-menu.css} +20 -19
- package/src/{less/containers/pairs-list.less → css/containers/pairs-list.css} +13 -12
- package/src/{themes/variables.less → css/containers/scroll-panel.css} +25 -22
- package/src/{less/containers/split-panel.less → css/containers/split-panel.css} +15 -14
- package/src/{less/containers/tabs.less → css/containers/tabs.css} +17 -19
- package/src/css/dom/checkbox.css +102 -0
- package/src/css/dom/scrollbar.css +35 -0
- package/src/{less/dom/select.less → css/dom/select.css} +17 -18
- package/src/{less/empty-column.less → css/empty-column.css} +19 -18
- package/src/{less/expression-editor.less → css/expression-editor.css} +19 -18
- package/src/{less/filter-dropdown.less → css/filter-dropdown.css} +12 -11
- package/src/{less/filter-item.less → css/filter-item.css} +16 -15
- package/src/{less/form/code-editor.less → css/form/code-editor.css} +26 -30
- package/src/{less/form/debug.less → css/form/debug.css} +19 -18
- package/src/{less/function-dropdown.less → css/function-dropdown.css} +12 -11
- package/src/css/plugin-selector.css +261 -0
- package/src/{less/render-warning.less → css/render-warning.css} +18 -17
- package/src/{less/status-bar.less → css/status-bar.css} +156 -144
- package/src/css/type-icon.css +116 -0
- package/src/{less/viewer.less → css/viewer.css} +112 -146
- package/src/rust/components/column_dropdown.rs +229 -119
- package/src/rust/components/column_selector/active_column.rs +81 -62
- package/src/rust/components/column_selector/add_expression_button.rs +1 -0
- package/src/rust/components/column_selector/aggregate_selector.rs +25 -15
- package/src/rust/components/column_selector/config_selector.rs +374 -185
- package/src/rust/components/column_selector/empty_column.rs +2 -2
- package/src/rust/components/column_selector/expr_edit_button.rs +8 -2
- package/src/rust/components/column_selector/filter_column.rs +37 -26
- package/src/rust/components/column_selector/inactive_column.rs +41 -29
- package/src/rust/components/column_selector/invalid_column.rs +7 -18
- package/src/rust/components/column_selector/pivot_column.rs +21 -10
- package/src/rust/components/column_selector/sort_column.rs +23 -13
- package/src/rust/components/column_selector.rs +189 -100
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +1 -1
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +3 -2
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +3 -2
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +2 -3
- package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +7 -1
- package/src/rust/components/column_settings_sidebar/style_tab.rs +153 -112
- package/src/rust/components/column_settings_sidebar.rs +91 -53
- package/src/rust/components/containers/dragdrop_list.rs +29 -7
- package/src/rust/components/containers/scroll_panel.rs +8 -1
- package/src/rust/components/containers/select.rs +3 -3
- package/src/rust/components/containers/sidebar_close_button.rs +1 -1
- package/src/rust/components/containers/split_panel.rs +3 -2
- package/src/rust/components/containers/tab_list.rs +1 -1
- package/src/rust/components/copy_dropdown.rs +7 -28
- package/src/rust/components/datetime_column_style/custom.rs +2 -2
- package/src/rust/components/datetime_column_style/simple.rs +2 -2
- package/src/rust/components/datetime_column_style.rs +4 -2
- package/src/rust/components/editable_header.rs +7 -4
- package/src/rust/components/empty_row.rs +1 -1
- package/src/rust/components/export_dropdown.rs +4 -30
- package/src/rust/components/expression_editor.rs +19 -10
- package/src/rust/components/filter_dropdown.rs +246 -102
- package/src/rust/components/font_loader.rs +11 -28
- package/src/rust/components/form/code_editor.rs +17 -2
- package/src/rust/components/form/color_range_selector.rs +19 -6
- package/src/rust/components/form/debug.rs +30 -13
- package/src/rust/components/function_dropdown.rs +186 -113
- package/src/rust/components/main_panel.rs +71 -89
- package/src/rust/components/mod.rs +1 -1
- package/src/rust/components/modal.rs +7 -1
- package/src/rust/components/number_column_style.rs +22 -7
- package/src/rust/components/plugin_selector.rs +34 -92
- package/src/rust/components/portal.rs +274 -0
- package/src/rust/components/render_warning.rs +72 -123
- package/src/rust/components/settings_panel.rs +115 -11
- package/src/rust/components/status_bar.rs +222 -98
- package/src/rust/components/status_bar_counter.rs +8 -20
- package/src/rust/components/status_indicator.rs +64 -111
- package/src/rust/components/string_column_style.rs +2 -2
- package/src/rust/components/style/style_cache.rs +5 -1
- package/src/rust/components/viewer.rs +391 -39
- package/src/rust/custom_elements/copy_dropdown.rs +102 -21
- package/src/rust/custom_elements/export_dropdown.rs +102 -20
- package/src/rust/custom_elements/mod.rs +0 -7
- package/src/rust/custom_elements/modal.rs +7 -103
- package/src/rust/custom_elements/viewer.rs +99 -35
- package/src/rust/custom_events.rs +23 -2
- package/src/rust/dragdrop.rs +149 -10
- package/src/{less/containers/scroll-panel.less → rust/engines.rs} +15 -13
- package/src/rust/js/plugin.rs +20 -1
- package/src/rust/lib.rs +5 -4
- package/src/rust/presentation/props.rs +39 -0
- package/src/rust/presentation/sheets.rs +3 -3
- package/src/rust/presentation.rs +44 -8
- package/src/rust/renderer/limits.rs +32 -3
- package/src/{less/dom/scrollbar.less → rust/renderer/props.rs} +18 -19
- package/src/rust/renderer/registry.rs +8 -1
- package/src/rust/renderer.rs +83 -9
- package/src/rust/session/column_defaults_update.rs +18 -0
- package/src/rust/session/metadata.rs +23 -2
- package/src/rust/session/props.rs +178 -0
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/session.rs +124 -117
- package/src/rust/tasks/column_locator.rs +133 -0
- package/src/rust/{model → tasks}/columns_iter_set.rs +14 -23
- package/src/rust/{model → tasks}/edit_expression.rs +34 -10
- package/src/rust/{model → tasks}/eject.rs +2 -2
- package/src/rust/{model → tasks}/get_viewer_config.rs +0 -11
- package/src/rust/{model → tasks}/intersection_observer.rs +22 -4
- package/src/{less/containers/radio-list.less → rust/tasks/is_invalid_drop.rs} +21 -14
- package/src/rust/tasks/mod.rs +52 -0
- package/src/rust/{model → tasks}/plugin_column_styles.rs +69 -46
- package/src/rust/{model → tasks}/resize_observer.rs +39 -6
- package/src/rust/{model → tasks}/send_plugin_config.rs +1 -1
- package/src/rust/tasks/structural.rs +53 -0
- package/src/rust/utils/mod.rs +4 -0
- package/src/rust/utils/modal_position.rs +110 -0
- package/src/rust/utils/ptr_eq_rc.rs +74 -0
- package/src/rust/utils/pubsub.rs +11 -1
- package/src/svg/bg-pattern.png +0 -0
- package/src/svg/close-icon.svg +1 -1
- package/src/svg/expression.svg +1 -1
- package/src/svg/mega-menu-icons-candlestick.svg +1 -1
- package/src/svg/mega-menu-icons-datagrid.svg +1 -2
- package/src/svg/mega-menu-icons-heatmap.svg +1 -1
- package/src/svg/mega-menu-icons-map-scatter.svg +1 -1
- package/src/svg/mega-menu-icons-ohlc.svg +1 -1
- package/src/svg/mega-menu-icons-sunburst.svg +1 -1
- package/src/svg/mega-menu-icons-treemap.svg +1 -1
- package/src/svg/mega-menu-icons-x-bar.svg +1 -1
- package/src/svg/mega-menu-icons-x-y-line.svg +1 -1
- package/src/svg/mega-menu-icons-x-y-scatter.svg +1 -1
- package/src/svg/mega-menu-icons-y-area.svg +1 -1
- package/src/svg/mega-menu-icons-y-bar.svg +1 -1
- package/src/svg/mega-menu-icons-y-line.svg +1 -1
- package/src/svg/mega-menu-icons-y-scatter.svg +1 -1
- package/src/svg/radio-hover.svg +1 -1
- package/src/svg/radio-off.svg +1 -1
- package/src/svg/radio-on.svg +1 -1
- package/src/themes/botanical.css +157 -0
- package/src/themes/defaults.css +139 -0
- package/src/themes/dracula.css +233 -0
- package/src/themes/gruvbox-dark.css +255 -0
- package/src/themes/gruvbox.css +134 -0
- package/src/themes/icons.css +124 -0
- package/src/themes/intl/de.css +102 -0
- package/src/themes/intl/es.css +102 -0
- package/src/themes/intl/fr.css +102 -0
- package/src/themes/intl/ja.css +102 -0
- package/src/themes/intl/pt.css +102 -0
- package/src/themes/intl/zh.css +102 -0
- package/src/themes/intl.css +102 -0
- package/src/themes/monokai.css +233 -0
- package/src/themes/pro-dark.css +158 -0
- package/src/themes/{themes.less → pro.css} +17 -20
- package/src/themes/solarized-dark.css +135 -0
- package/src/themes/solarized.css +95 -0
- package/src/themes/themes.css +22 -0
- package/src/themes/vaporwave.css +256 -0
- package/src/ts/extensions.ts +73 -2
- package/src/ts/perspective-viewer.ts +1 -0
- package/src/ts/ts-rs/GroupRollupMode.ts +3 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +2 -1
- package/tsconfig.json +1 -0
- package/dist/css/variables.css +0 -0
- package/src/less/column-dropdown.less +0 -95
- package/src/less/config-selector.less +0 -363
- package/src/less/dom/checkbox.less +0 -100
- package/src/less/plugin-selector.less +0 -183
- package/src/less/type-icon.less +0 -68
- package/src/rust/components/error_message.rs +0 -56
- package/src/rust/custom_elements/column_dropdown.rs +0 -123
- package/src/rust/custom_elements/filter_dropdown.rs +0 -179
- package/src/rust/custom_elements/function_dropdown.rs +0 -115
- package/src/rust/model/column_locator.rs +0 -82
- package/src/rust/model/is_invalid_drop.rs +0 -36
- package/src/rust/model/mod.rs +0 -100
- package/src/rust/model/reset_all.rs +0 -38
- package/src/rust/model/structural.rs +0 -244
- package/src/themes/dracula.less +0 -101
- package/src/themes/gruvbox-dark.less +0 -116
- package/src/themes/gruvbox.less +0 -152
- package/src/themes/icons.less +0 -130
- package/src/themes/intl/de.less +0 -102
- package/src/themes/intl/es.less +0 -102
- package/src/themes/intl/fr.less +0 -102
- package/src/themes/intl/ja.less +0 -102
- package/src/themes/intl/pt.less +0 -102
- package/src/themes/intl/zh.less +0 -102
- package/src/themes/intl.less +0 -102
- package/src/themes/monokai.less +0 -107
- package/src/themes/pro-dark.less +0 -147
- package/src/themes/pro.less +0 -186
- package/src/themes/solarized-dark.less +0 -78
- package/src/themes/solarized.less +0 -102
- package/src/themes/vaporwave.less +0 -145
- /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline4.js +0 -0
- /package/src/rust/{model → tasks}/copy_export.rs +0 -0
- /package/src/rust/{model → tasks}/export_app.rs +0 -0
- /package/src/rust/{model → tasks}/export_method.rs +0 -0
- /package/src/rust/{model → tasks}/restore_and_render.rs +0 -0
- /package/src/rust/{model → tasks}/update_and_render.rs +0 -0
|
@@ -10,96 +10,52 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
-
use perspective_client::config::ViewConfigUpdate;
|
|
14
|
-
use perspective_js::utils::ApiFuture;
|
|
15
13
|
use yew::prelude::*;
|
|
16
14
|
|
|
17
|
-
use super::containers::select::*;
|
|
18
15
|
use super::style::LocalStyle;
|
|
19
|
-
use crate::
|
|
20
|
-
use crate::
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
use crate::utils::*;
|
|
26
|
-
use crate::{css, *};
|
|
27
|
-
|
|
28
|
-
#[derive(Properties, PartialEq, PerspectiveProperties!)]
|
|
16
|
+
use crate::css;
|
|
17
|
+
use crate::utils::PtrEqRc;
|
|
18
|
+
|
|
19
|
+
/// Pure value props — no engine handles, no PubSub subscriptions.
|
|
20
|
+
/// The parent passes updated values whenever the renderer state changes.
|
|
21
|
+
#[derive(Properties, PartialEq)]
|
|
29
22
|
pub struct PluginSelectorProps {
|
|
30
|
-
|
|
31
|
-
pub
|
|
32
|
-
|
|
23
|
+
/// Name of the currently active plugin.
|
|
24
|
+
pub plugin_name: Option<String>,
|
|
25
|
+
|
|
26
|
+
/// Flat list of all registered plugin names (all categories merged).
|
|
27
|
+
pub available_plugins: PtrEqRc<Vec<String>>,
|
|
28
|
+
|
|
29
|
+
/// Called when the user selects a different plugin.
|
|
30
|
+
pub on_select_plugin: Callback<String>,
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
#[derive(Debug)]
|
|
36
34
|
pub enum PluginSelectorMsg {
|
|
37
35
|
ComponentSelectPlugin(String),
|
|
38
|
-
RendererSelectPlugin(String),
|
|
39
36
|
OpenMenu,
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
use PluginSelectorMsg::*;
|
|
43
40
|
|
|
44
41
|
pub struct PluginSelector {
|
|
45
|
-
options: Vec<SelectItem<String>>,
|
|
46
42
|
is_open: bool,
|
|
47
|
-
_plugin_sub: Subscription,
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
impl Component for PluginSelector {
|
|
51
46
|
type Message = PluginSelectorMsg;
|
|
52
47
|
type Properties = PluginSelectorProps;
|
|
53
48
|
|
|
54
|
-
fn create(
|
|
55
|
-
|
|
56
|
-
let options = generate_plugin_optgroups(renderer);
|
|
57
|
-
let _plugin_sub = renderer.plugin_changed.add_listener({
|
|
58
|
-
let link = ctx.link().clone();
|
|
59
|
-
move |plugin: JsPerspectiveViewerPlugin| {
|
|
60
|
-
let name = plugin.name();
|
|
61
|
-
link.send_message(PluginSelectorMsg::RendererSelectPlugin(name))
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
Self {
|
|
66
|
-
options,
|
|
67
|
-
is_open: false,
|
|
68
|
-
_plugin_sub,
|
|
69
|
-
}
|
|
49
|
+
fn create(_ctx: &Context<Self>) -> Self {
|
|
50
|
+
Self { is_open: false }
|
|
70
51
|
}
|
|
71
52
|
|
|
72
53
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
73
|
-
let PluginSelectorProps {
|
|
74
|
-
presentation,
|
|
75
|
-
renderer,
|
|
76
|
-
session,
|
|
77
|
-
..
|
|
78
|
-
} = ctx.props();
|
|
79
54
|
match msg {
|
|
80
|
-
RendererSelectPlugin(_plugin_name) => true,
|
|
81
55
|
ComponentSelectPlugin(plugin_name) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let mut update = ViewConfigUpdate::default();
|
|
87
|
-
session.set_update_column_defaults(
|
|
88
|
-
&mut update,
|
|
89
|
-
metadata.as_ref().unwrap_or(&*renderer.metadata()),
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
if let Ok(task) = ctx.props().update_and_render(update) {
|
|
93
|
-
ApiFuture::spawn(task);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
presentation.set_open_column_settings(None);
|
|
97
|
-
self.is_open = false;
|
|
98
|
-
false
|
|
99
|
-
} else {
|
|
100
|
-
self.is_open = false;
|
|
101
|
-
true
|
|
102
|
-
}
|
|
56
|
+
ctx.props().on_select_plugin.emit(plugin_name);
|
|
57
|
+
self.is_open = false;
|
|
58
|
+
false
|
|
103
59
|
},
|
|
104
60
|
OpenMenu => {
|
|
105
61
|
self.is_open = !self.is_open;
|
|
@@ -114,22 +70,18 @@ impl Component for PluginSelector {
|
|
|
114
70
|
|
|
115
71
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
116
72
|
let callback = ctx.link().callback(|_| OpenMenu);
|
|
117
|
-
let plugin_name = ctx.props().
|
|
73
|
+
let plugin_name = ctx.props().plugin_name.clone().unwrap_or_default();
|
|
118
74
|
let plugin_name2 = plugin_name.clone();
|
|
119
75
|
let class = if self.is_open { "open" } else { "" };
|
|
120
|
-
let items =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}).collect::<Html>()
|
|
130
|
-
},
|
|
131
|
-
SelectItem::Option(_item) => html! {},
|
|
132
|
-
});
|
|
76
|
+
let items = ctx
|
|
77
|
+
.props()
|
|
78
|
+
.available_plugins
|
|
79
|
+
.iter()
|
|
80
|
+
.filter(|x| x.as_str() != plugin_name2.as_str())
|
|
81
|
+
.map(|x| {
|
|
82
|
+
let callback = ctx.link().callback(ComponentSelectPlugin);
|
|
83
|
+
html! { <PluginSelect name={x.to_owned()} on_click={callback} /> }
|
|
84
|
+
});
|
|
133
85
|
|
|
134
86
|
html! {
|
|
135
87
|
<>
|
|
@@ -138,7 +90,9 @@ impl Component for PluginSelector {
|
|
|
138
90
|
<PluginSelect name={plugin_name} on_click={callback} />
|
|
139
91
|
<div id="plugin_selector_border" />
|
|
140
92
|
if self.is_open {
|
|
141
|
-
<div class="plugin-selector-options">
|
|
93
|
+
<div class="plugin-selector-options scrollable">
|
|
94
|
+
{ items.collect::<Html>() }
|
|
95
|
+
</div>
|
|
142
96
|
}
|
|
143
97
|
</div>
|
|
144
98
|
</>
|
|
@@ -146,19 +100,6 @@ impl Component for PluginSelector {
|
|
|
146
100
|
}
|
|
147
101
|
}
|
|
148
102
|
|
|
149
|
-
/// Generate the opt groups for the plugin selector by collecting by category
|
|
150
|
-
/// then sorting.
|
|
151
|
-
fn generate_plugin_optgroups(renderer: &Renderer) -> Vec<SelectItem<String>> {
|
|
152
|
-
let mut options = renderer
|
|
153
|
-
.get_all_plugin_categories()
|
|
154
|
-
.into_iter()
|
|
155
|
-
.map(|(category, value)| SelectItem::OptGroup(category.into(), value))
|
|
156
|
-
.collect::<Vec<_>>();
|
|
157
|
-
|
|
158
|
-
options.sort_by_key(|x| x.name());
|
|
159
|
-
options
|
|
160
|
-
}
|
|
161
|
-
|
|
162
103
|
#[derive(Properties, PartialEq)]
|
|
163
104
|
struct PluginSelectProps {
|
|
164
105
|
name: String,
|
|
@@ -184,9 +125,10 @@ fn PluginSelect(props: &PluginSelectProps) -> Html {
|
|
|
184
125
|
<div
|
|
185
126
|
class="plugin-select-item"
|
|
186
127
|
data-plugin={name.clone()}
|
|
187
|
-
style={format!("--default-column-title:var(--plugin-name
|
|
128
|
+
style={format!("--default-column-title:var(--psp-plugin-name--{}--content, \"{}\")", path, props.name)}
|
|
188
129
|
onclick={props.on_click.reform(move |_| name.clone())}
|
|
189
130
|
>
|
|
131
|
+
<span class="icon" />
|
|
190
132
|
<span class="plugin-select-item-name" />
|
|
191
133
|
</div>
|
|
192
134
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
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_js::utils::global;
|
|
17
|
+
use wasm_bindgen::JsCast;
|
|
18
|
+
use wasm_bindgen::prelude::*;
|
|
19
|
+
use web_sys::*;
|
|
20
|
+
use yew::prelude::*;
|
|
21
|
+
|
|
22
|
+
use crate::components::modal::ModalOrientation;
|
|
23
|
+
use crate::components::style::StyleProvider;
|
|
24
|
+
use crate::utils::*;
|
|
25
|
+
|
|
26
|
+
#[derive(Properties, PartialEq)]
|
|
27
|
+
pub struct PortalModalProps {
|
|
28
|
+
pub children: Children,
|
|
29
|
+
|
|
30
|
+
/// The element to position relative to. `None` means closed.
|
|
31
|
+
pub target: Option<HtmlElement>,
|
|
32
|
+
|
|
33
|
+
/// Whether the portal manages its own focus and closes on blur.
|
|
34
|
+
#[prop_or(true)]
|
|
35
|
+
pub own_focus: bool,
|
|
36
|
+
|
|
37
|
+
/// Called when the portal closes (blur, etc).
|
|
38
|
+
#[prop_or_default]
|
|
39
|
+
pub on_close: Callback<()>,
|
|
40
|
+
|
|
41
|
+
pub tag_name: &'static str,
|
|
42
|
+
|
|
43
|
+
pub theme: String,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub enum PortalModalMsg {
|
|
47
|
+
Reposition,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub struct PortalModal {
|
|
51
|
+
host: HtmlElement,
|
|
52
|
+
shadow_root: Element,
|
|
53
|
+
top: f64,
|
|
54
|
+
left: f64,
|
|
55
|
+
visible: bool,
|
|
56
|
+
rev_vert: ModalOrientation,
|
|
57
|
+
anchor: Rc<Cell<ModalAnchor>>,
|
|
58
|
+
_blur_closure: Option<Closure<dyn FnMut(FocusEvent)>>,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl PortalModal {
|
|
62
|
+
fn attach_to_body(&self) {
|
|
63
|
+
if !self.host.is_connected() {
|
|
64
|
+
let _ = global::body().append_child(&self.host);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn detach_from_body(&mut self) {
|
|
69
|
+
if self.host.is_connected() {
|
|
70
|
+
let _ = global::body().remove_child(&self.host);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if let Some(closure) = self._blur_closure.as_ref() {
|
|
74
|
+
self.host
|
|
75
|
+
.remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
|
|
76
|
+
.unwrap()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
self._blur_closure = None;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn position_against_target(&mut self, target: &HtmlElement) {
|
|
83
|
+
let target_rect = target.get_bounding_client_rect();
|
|
84
|
+
let height = target_rect.height();
|
|
85
|
+
let width = target_rect.width();
|
|
86
|
+
let top = target_rect.top();
|
|
87
|
+
let left = target_rect.left();
|
|
88
|
+
|
|
89
|
+
if !self.visible {
|
|
90
|
+
// First pass: position at default anchor, invisible
|
|
91
|
+
self.top = top + height - 1.0;
|
|
92
|
+
self.left = left;
|
|
93
|
+
self.visible = false;
|
|
94
|
+
} else {
|
|
95
|
+
// Second pass: compute actual anchor and reposition
|
|
96
|
+
let anchor = calc_relative_position(&self.host, top, left, height, width);
|
|
97
|
+
self.anchor.set(anchor);
|
|
98
|
+
let modal_rect = self.host.get_bounding_client_rect();
|
|
99
|
+
let (new_top, new_left) = calc_anchor_position(anchor, &target_rect, &modal_rect);
|
|
100
|
+
self.top = new_top;
|
|
101
|
+
self.left = new_left;
|
|
102
|
+
self.rev_vert.set(anchor.is_rev_vert());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn setup_blur_handler(&mut self, ctx: &Context<Self>) {
|
|
107
|
+
let on_close = {
|
|
108
|
+
let target = ctx.props().target.clone();
|
|
109
|
+
ctx.props().on_close.reform(move |_| {
|
|
110
|
+
if let Some(target) = &target {
|
|
111
|
+
target.class_list().remove_1("modal-target").unwrap();
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let closure = Closure::wrap(Box::new(move |_: FocusEvent| {
|
|
117
|
+
on_close.emit(());
|
|
118
|
+
}) as Box<dyn FnMut(FocusEvent)>);
|
|
119
|
+
|
|
120
|
+
let _ = self
|
|
121
|
+
.host
|
|
122
|
+
.add_event_listener_with_callback("blur", closure.as_ref().unchecked_ref());
|
|
123
|
+
|
|
124
|
+
self._blur_closure = Some(closure);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl Component for PortalModal {
|
|
129
|
+
type Message = PortalModalMsg;
|
|
130
|
+
type Properties = PortalModalProps;
|
|
131
|
+
|
|
132
|
+
fn create(ctx: &Context<Self>) -> Self {
|
|
133
|
+
let host: HtmlElement = global::document()
|
|
134
|
+
.create_element(ctx.props().tag_name)
|
|
135
|
+
.unwrap()
|
|
136
|
+
.unchecked_into();
|
|
137
|
+
|
|
138
|
+
host.style().set_property("position", "fixed").unwrap();
|
|
139
|
+
host.style().set_property("z-index", "10000").unwrap();
|
|
140
|
+
let init = ShadowRootInit::new(ShadowRootMode::Open);
|
|
141
|
+
let shadow_root = if let Some(elem) = host.shadow_root() {
|
|
142
|
+
elem
|
|
143
|
+
} else {
|
|
144
|
+
host.attach_shadow(&init).unwrap()
|
|
145
|
+
}
|
|
146
|
+
.unchecked_into::<Element>();
|
|
147
|
+
|
|
148
|
+
Self {
|
|
149
|
+
host,
|
|
150
|
+
shadow_root,
|
|
151
|
+
top: 0.0,
|
|
152
|
+
left: 0.0,
|
|
153
|
+
visible: false,
|
|
154
|
+
rev_vert: Default::default(),
|
|
155
|
+
anchor: Default::default(),
|
|
156
|
+
_blur_closure: None,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
161
|
+
match msg {
|
|
162
|
+
PortalModalMsg::Reposition => {
|
|
163
|
+
self.visible = true;
|
|
164
|
+
true
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
|
|
170
|
+
let new_target = &ctx.props().target;
|
|
171
|
+
let old_target = &old_props.target;
|
|
172
|
+
|
|
173
|
+
match (old_target, new_target, self._blur_closure.as_ref()) {
|
|
174
|
+
(None, Some(_), Some(closure)) => {
|
|
175
|
+
self.visible = false;
|
|
176
|
+
self.host
|
|
177
|
+
.remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
|
|
178
|
+
.unwrap();
|
|
179
|
+
|
|
180
|
+
self._blur_closure = None;
|
|
181
|
+
},
|
|
182
|
+
(None, Some(_), None) => {
|
|
183
|
+
self.visible = false;
|
|
184
|
+
self._blur_closure = None;
|
|
185
|
+
},
|
|
186
|
+
(Some(_), None, _) => {
|
|
187
|
+
self.detach_from_body();
|
|
188
|
+
return true;
|
|
189
|
+
},
|
|
190
|
+
_ => {},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
197
|
+
let target = &ctx.props().target;
|
|
198
|
+
if target.is_none() {
|
|
199
|
+
return html! {};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let opacity = if self.visible { "" } else { ";opacity:0" };
|
|
203
|
+
let css = format!(
|
|
204
|
+
":host{{top:{}px;left:{}px{}}}",
|
|
205
|
+
self.top, self.left, opacity
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
let portal_content = html! {
|
|
209
|
+
<>
|
|
210
|
+
<style>{ css }</style>
|
|
211
|
+
<ContextProvider<ModalOrientation> context={self.rev_vert.clone()}>
|
|
212
|
+
<StyleProvider root={self.host.clone()}>
|
|
213
|
+
{ for ctx.props().children.iter() }
|
|
214
|
+
</StyleProvider>
|
|
215
|
+
</ContextProvider<ModalOrientation>>
|
|
216
|
+
</>
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
yew::create_portal(portal_content, self.shadow_root.clone())
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
|
|
223
|
+
if let Some(target) = &ctx.props().target {
|
|
224
|
+
if !self.host.is_connected() {
|
|
225
|
+
let theme = ctx.props().theme.as_str();
|
|
226
|
+
self.host.set_attribute("theme", theme).unwrap();
|
|
227
|
+
|
|
228
|
+
// First render with a target: attach to body, position invisible
|
|
229
|
+
self.position_against_target(target);
|
|
230
|
+
self.attach_to_body();
|
|
231
|
+
|
|
232
|
+
// Propagate theme from target
|
|
233
|
+
if let Some(theme) = target.get_attribute("theme") {
|
|
234
|
+
let _ = self.host.set_attribute("theme", &theme);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
target.class_list().add_1("modal-target").unwrap();
|
|
238
|
+
|
|
239
|
+
if ctx.props().own_focus {
|
|
240
|
+
self.host.set_attribute("tabindex", "0").unwrap();
|
|
241
|
+
self.setup_blur_handler(ctx);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Schedule second positioning pass
|
|
245
|
+
let link = ctx.link().clone();
|
|
246
|
+
wasm_bindgen_futures::spawn_local(async move {
|
|
247
|
+
request_animation_frame().await;
|
|
248
|
+
link.send_message(PortalModalMsg::Reposition);
|
|
249
|
+
});
|
|
250
|
+
} else if self.visible {
|
|
251
|
+
// Second pass: reposition with correct anchor
|
|
252
|
+
self.position_against_target(target);
|
|
253
|
+
|
|
254
|
+
if ctx.props().own_focus && self._blur_closure.is_some() {
|
|
255
|
+
let _ = self.host.focus();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fn destroy(&mut self, ctx: &Context<Self>) {
|
|
262
|
+
if let Some(target) = &ctx.props().target {
|
|
263
|
+
target.class_list().remove_1("modal-target").unwrap();
|
|
264
|
+
if target.get_attribute("theme").is_some() {
|
|
265
|
+
let _ = self.host.remove_attribute("theme");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let event = CustomEvent::new("-perspective-close-expression").unwrap();
|
|
269
|
+
let _ = target.dispatch_event(&event);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
self.detach_from_body();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -13,144 +13,93 @@
|
|
|
13
13
|
use yew::prelude::*;
|
|
14
14
|
|
|
15
15
|
use super::style::LocalStyle;
|
|
16
|
-
use crate::
|
|
17
|
-
use crate::renderer
|
|
18
|
-
use crate::session::*;
|
|
19
|
-
use crate::*;
|
|
16
|
+
use crate::css;
|
|
17
|
+
use crate::renderer::limits::RenderLimits;
|
|
20
18
|
|
|
21
|
-
#[derive(Properties,
|
|
19
|
+
#[derive(Properties, PartialEq)]
|
|
22
20
|
pub struct RenderWarningProps {
|
|
23
|
-
|
|
24
|
-
pub dimensions: Option<(usize, usize, Option<usize>, Option<usize>)>,
|
|
21
|
+
pub dimensions: Option<RenderLimits>,
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
pub
|
|
23
|
+
/// Called when the user clicks "Render all points". The parent disables
|
|
24
|
+
/// the render warning on the active plugin and re-draws.
|
|
25
|
+
pub on_dismiss: Callback<()>,
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
pub enum RenderWarningMsg {
|
|
38
|
-
DismissWarning,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
pub struct RenderWarning {
|
|
42
|
-
col_warn: Option<(usize, usize)>,
|
|
43
|
-
row_warn: Option<(usize, usize)>,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
impl RenderWarning {
|
|
47
|
-
fn update_warnings(&mut self, ctx: &Context<Self>) {
|
|
48
|
-
if let Some((num_cols, num_rows, max_cols, max_rows)) = ctx.props().dimensions {
|
|
49
|
-
let count = num_cols * num_rows;
|
|
50
|
-
if max_cols.is_some_and(|x| x < num_cols) {
|
|
51
|
-
self.col_warn = Some((max_cols.unwrap(), num_cols));
|
|
52
|
-
} else {
|
|
53
|
-
self.col_warn = None;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if max_rows.is_some_and(|x| x < num_rows) {
|
|
57
|
-
self.row_warn = Some((num_cols * max_rows.unwrap(), count));
|
|
58
|
-
} else {
|
|
59
|
-
self.row_warn = None;
|
|
60
|
-
}
|
|
28
|
+
#[function_component(RenderWarning)]
|
|
29
|
+
pub fn render_warning(props: &RenderWarningProps) -> Html {
|
|
30
|
+
let dimensions = props.dimensions;
|
|
31
|
+
let (col_warn, row_warn) = if let Some(limits) = dimensions {
|
|
32
|
+
let col_warn = if limits.max_cols.is_some_and(|x| x < limits.num_cols) {
|
|
33
|
+
Some((limits.max_cols.unwrap(), limits.num_cols))
|
|
61
34
|
} else {
|
|
62
|
-
|
|
63
|
-
self.row_warn = None;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
impl Component for RenderWarning {
|
|
69
|
-
type Message = RenderWarningMsg;
|
|
70
|
-
type Properties = RenderWarningProps;
|
|
71
|
-
|
|
72
|
-
fn create(ctx: &Context<Self>) -> Self {
|
|
73
|
-
let mut elem = Self {
|
|
74
|
-
col_warn: None,
|
|
75
|
-
row_warn: None,
|
|
35
|
+
None
|
|
76
36
|
};
|
|
77
37
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
38
|
+
let row_warn = if limits.max_rows.is_some_and(|x| x < limits.num_rows) {
|
|
39
|
+
Some((
|
|
40
|
+
limits.num_cols * limits.max_rows.unwrap(),
|
|
41
|
+
limits.num_cols * limits.num_rows,
|
|
42
|
+
))
|
|
43
|
+
} else {
|
|
44
|
+
None
|
|
45
|
+
};
|
|
81
46
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
47
|
+
(col_warn, row_warn)
|
|
48
|
+
} else {
|
|
49
|
+
(None, None)
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if col_warn.is_some() || row_warn.is_some() {
|
|
53
|
+
let warning = match (col_warn, row_warn) {
|
|
54
|
+
(Some((x, y)), Some((a, b))) => html! {
|
|
55
|
+
<span style="white-space: nowrap">
|
|
56
|
+
{ "Rendering" }
|
|
57
|
+
{ render_pair(x, y) }
|
|
58
|
+
{ "of columns and" }
|
|
59
|
+
{ render_pair(a, b) }
|
|
60
|
+
{ "of points." }
|
|
61
|
+
</span>
|
|
62
|
+
},
|
|
63
|
+
(Some((x, y)), None) => html! {
|
|
64
|
+
<span style="white-space: nowrap">
|
|
65
|
+
{ "Rendering" }
|
|
66
|
+
{ render_pair(x, y) }
|
|
67
|
+
{ "of columns." }
|
|
68
|
+
</span>
|
|
69
|
+
},
|
|
70
|
+
(None, Some((x, y))) => html! {
|
|
71
|
+
<span style="white-space: nowrap">
|
|
72
|
+
{ "Rendering" }
|
|
73
|
+
{ render_pair(x, y) }
|
|
74
|
+
{ "of points." }
|
|
75
|
+
</span>
|
|
91
76
|
},
|
|
77
|
+
_ => html! { <div /> },
|
|
92
78
|
};
|
|
93
|
-
true
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
|
|
97
|
-
self.update_warnings(ctx);
|
|
98
|
-
true
|
|
99
|
-
}
|
|
100
79
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
80
|
+
let on_dismiss = props.on_dismiss.clone();
|
|
81
|
+
let onclick = Callback::from(move |_: MouseEvent| on_dismiss.emit(()));
|
|
82
|
+
html! {
|
|
83
|
+
<>
|
|
84
|
+
<LocalStyle href={css!("render-warning")} />
|
|
85
|
+
<div
|
|
86
|
+
class="plugin_information plugin_information--warning"
|
|
87
|
+
id="plugin_information--size"
|
|
88
|
+
>
|
|
89
|
+
<span class="plugin_information__icon" />
|
|
90
|
+
<span class="plugin_information__text" id="plugin_information_count">
|
|
91
|
+
{ warning }
|
|
111
92
|
</span>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
{ "Rendering" }
|
|
116
|
-
{ render_pair(x, y) }
|
|
117
|
-
{ "of columns." }
|
|
118
|
-
</span>
|
|
119
|
-
},
|
|
120
|
-
(None, Some((x, y))) => html! {
|
|
121
|
-
<span style="white-space: nowrap">
|
|
122
|
-
{ "Rendering" }
|
|
123
|
-
{ render_pair(x, y) }
|
|
124
|
-
{ "of points." }
|
|
125
|
-
</span>
|
|
126
|
-
},
|
|
127
|
-
_ => html! { <div /> },
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
let onclick = ctx.link().callback(|_| RenderWarningMsg::DismissWarning);
|
|
131
|
-
|
|
132
|
-
html! {
|
|
133
|
-
<>
|
|
134
|
-
<LocalStyle href={css!("render-warning")} />
|
|
135
|
-
<div
|
|
136
|
-
class="plugin_information plugin_information--warning"
|
|
137
|
-
id="plugin_information--size"
|
|
138
|
-
>
|
|
139
|
-
<span class="plugin_information__icon" />
|
|
140
|
-
<span class="plugin_information__text" id="plugin_information_count">
|
|
141
|
-
{ warning }
|
|
93
|
+
<span class="plugin_information__actions">
|
|
94
|
+
<span class="plugin_information__action" onmousedown={onclick}>
|
|
95
|
+
{ "Render all points" }
|
|
142
96
|
</span>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</span>
|
|
147
|
-
</span>
|
|
148
|
-
</div>
|
|
149
|
-
</>
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
html! {}
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
</>
|
|
153
100
|
}
|
|
101
|
+
} else {
|
|
102
|
+
html! {}
|
|
154
103
|
}
|
|
155
104
|
}
|
|
156
105
|
|