diff --git a/examples/blocks/src/covid/index.html b/examples/blocks/src/covid/index.html index 028281a89f..9527658fea 100644 --- a/examples/blocks/src/covid/index.html +++ b/examples/blocks/src/covid/index.html @@ -43,9 +43,9 @@ sort: [["deathIncrease", "col desc"]], expressions: { [`Parsed "date" bucket by week`]: `var year := integer(floor("date" / 10000)); - var month := integer(floor("date" / 100)) - year * 100; - var day := integer("date" % 100); - bucket(date(year, month, day), \'W\')`, +var month := integer(floor("date" / 100)) - year * 100; +var day := integer("date" % 100); +bucket(date(year, month, day), \'W\')`, }, aggregates: {}, }; diff --git a/examples/blocks/src/dataset/index.html b/examples/blocks/src/dataset/index.html new file mode 100644 index 0000000000..8413fd9271 --- /dev/null +++ b/examples/blocks/src/dataset/index.html @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + +
+
+ +
+ Rows + + +
+
+ Float columns + +
+
+ Integer columns + +
+
+ String columns + + +
+
+ Datetime columns + +
+
+ Bool columns + +
+
+ + + + +
+ + + + \ No newline at end of file diff --git a/packages/perspective-viewer-datagrid/src/js/style_handlers/column_header.js b/packages/perspective-viewer-datagrid/src/js/style_handlers/column_header.js index 0c41f4874b..433cf7952d 100644 --- a/packages/perspective-viewer-datagrid/src/js/style_handlers/column_header.js +++ b/packages/perspective-viewer-datagrid/src/js/style_handlers/column_header.js @@ -59,7 +59,7 @@ export function style_selected_column(regularTable, viewer, selectedColumn) { const title = titles[i]; const editBtn = editBtns[i]; - let open = title.innerText === selectedColumn; + let open = title.textContent === selectedColumn; title.classList.toggle("psp-menu-open", open); editBtn.classList.toggle("psp-menu-open", open); if (this._config.columns.length > 1) { diff --git a/rust/perspective-viewer/src/less/containers/tabs.less b/rust/perspective-viewer/src/less/containers/tabs.less index a94d8dfbee..fb0d953995 100644 --- a/rust/perspective-viewer/src/less/containers/tabs.less +++ b/rust/perspective-viewer/src/less/containers/tabs.less @@ -62,6 +62,11 @@ } } } + + #format-tab { + overflow: scroll; + } + .tab-content { @include scrollbar; flex: 1 1 auto; diff --git a/rust/perspective-viewer/src/less/dom/scrollbar.less b/rust/perspective-viewer/src/less/dom/scrollbar.less index 0d57dd1cba..d74d4bd1b4 100644 --- a/rust/perspective-viewer/src/less/dom/scrollbar.less +++ b/rust/perspective-viewer/src/less/dom/scrollbar.less @@ -11,18 +11,25 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ @mixin scrollbar { - &::-webkit-scrollbar-thumb { - border: 0 solid var(--icon--color); - border-left-width: 1px; - } + &:hover { + --fix: ; + } - &::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0.1); - } + &::-webkit-scrollbar-thumb { + border-radius: 2px; + border: 2px solid transparent; + box-shadow: inset 0px 0px 0 4px var(--inactive--color); + } - &::-webkit-scrollbar, - &::-webkit-scrollbar-corner { - background-color: transparent; - width: 6px; - } -} \ No newline at end of file + &:hover::-webkit-scrollbar-thumb { + border: 1px solid transparent; + box-shadow: inset 0px 0px 0 4px var(--inactive--color); + } + + &::-webkit-scrollbar, + &::-webkit-scrollbar-corner { + background-color: transparent; + width: 6px; + height: 6px; + } +} diff --git a/rust/perspective-viewer/src/less/form/code-editor.less b/rust/perspective-viewer/src/less/form/code-editor.less index e6b5c165ec..74992a6bd6 100644 --- a/rust/perspective-viewer/src/less/form/code-editor.less +++ b/rust/perspective-viewer/src/less/form/code-editor.less @@ -10,6 +10,8 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +@import "dom/scrollbar.less"; + :host { #editor { display: flex; @@ -22,21 +24,24 @@ } #editor-inner { + overflow: hidden; position: relative; display: flex; flex: 1 1 auto; } #line_numbers { - pointer-events: none; - padding: 6px 0 6px 6px; + color: var(--inactive--color); + background-color: var(--plugin--background, white); box-sizing: border-box; - font-weight: 400; - font-family: var(--interface-monospace--font-family, monospace); display: flex; flex-direction: column; + flex: 0 0 auto; + font-family: var(--interface-monospace--font-family, monospace); + font-weight: 400; overflow: hidden; - background-color: var(--plugin--background, white); + padding: 6px 0 6px 6px; + pointer-events: none; span:after { color: var(--code-editor-error--color, red); white-space: pre; @@ -50,7 +55,7 @@ } pre#content { - margin: 0; + margin: 0 6px 6px 0; padding: 6px 0; // margin-left: 36px; box-sizing: border-box; @@ -100,8 +105,10 @@ } #textarea_editable { + @include scrollbar; + position: absolute; - width: calc(100% - 36px); + width: 100%; height: 100%; font-family: var(--interface-monospace--font-family, monospace); font-size: 1em; diff --git a/rust/perspective-viewer/src/less/form/debug.less b/rust/perspective-viewer/src/less/form/debug.less new file mode 100644 index 0000000000..07f182525e --- /dev/null +++ b/rust/perspective-viewer/src/less/form/debug.less @@ -0,0 +1,88 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +:host { + div.tab-title#Debug:before { + content: var(--debug-tab-label--content, "Debug JSON"); + } + + #debug-panel-overflow { + overflow: hidden; + display: flex; + } + + .tab-padding { + border-right: none; + } + + .tab-title { + height: 36px; + } + + #debug-panel { + display: flex; + min-width: 150px; + label { + display: block; + margin-bottom: 2px; + font-size: var(--label--font-size, 0.75em); + } + + #debug-panel-controls { + display: flex; + flex: 0 0 auto; + gap: 0.333333em; + padding: 0px 8px 12px 8px; + align-items: center; + button { + appearance: none; + background-color: transparent; + border-radius: 3px; + border: 1px solid var(--icon--color); + color: inherit; + cursor: pointer; + flex: 0 1 100px; + font-family: inherit; + font-size: 0.8333em; + text-transform: uppercase; + height: 24px; + text-overflow: ellipsis; + overflow: hidden; + width: 0px; + &:not([disabled]):hover { + background-color: var(--icon--color); + color: var(--plugin--background); + } + + &[disabled] { + cursor: not-allowed; + opacity: 0.2; + } + } + } + + #debug-panel-editor { + display: flex; + flex-direction: column; + flex: 0 1 auto; + overflow: hidden; + padding: 12px 8px 12px 8px; + } + + #editor { + background-color: var(--plugin--background); + border: 1px solid var(--inactive--color); + border-radius: 2px; + // height: 100%; + } + } +} diff --git a/rust/perspective-viewer/src/less/viewer.less b/rust/perspective-viewer/src/less/viewer.less index 66db89b2ad..75db84eec1 100644 --- a/rust/perspective-viewer/src/less/viewer.less +++ b/rust/perspective-viewer/src/less/viewer.less @@ -55,6 +55,49 @@ left: -26px; } +:host #debug_close_button.sidebar_close_button .sidebar_close_button_inner { + background-attachment: fixed; + background-color: var(--plugin--background); + background-image: url(../svg/bg-pattern.png); + background-size: 4px; + + &:before { + mask-image: url(../svg/drawer-tab-hover.svg); + -webkit-mask-image: url(../svg/drawer-tab-hover.svg); + } +} + +:host + #debug_close_button.sidebar_close_button:hover + .sidebar_close_button_inner:before { + mask-image: url(../svg/drawer-tab.svg); + -webkit-mask-image: url(../svg/drawer-tab.svg); +} + +:host #debug_open_button.sidebar_close_button { + right: 0px; + cursor: pointer; + .sidebar_close_button_inner { + background-color: transparent; + } +} + +:host #debug_open_button.sidebar_close_button .sidebar_close_button_inner { + &:before { + mask-image: url(../svg/drawer-tab-invert.svg); + -webkit-mask-image: url(../svg/drawer-tab-invert.svg); + } + + &:hover:before { + mask-image: url(../svg/drawer-tab-invert-hover.svg); + -webkit-mask-image: url(../svg/drawer-tab-invert-hover.svg); + } +} + +:host #debug_close_button.sidebar_close_button { + right: 0px; +} + :host(:hover) { #menu-bar { opacity: 1 !important; diff --git a/rust/perspective-viewer/src/rust/components/containers/mod.rs b/rust/perspective-viewer/src/rust/components/containers/mod.rs index 2ad676bacd..54973ac2f9 100644 --- a/rust/perspective-viewer/src/rust/components/containers/mod.rs +++ b/rust/perspective-viewer/src/rust/components/containers/mod.rs @@ -22,6 +22,7 @@ pub mod select; pub mod sidebar; pub mod split_panel; pub mod tab_list; +pub mod trap_door_panel; #[cfg(test)] mod tests; diff --git a/rust/perspective-viewer/src/rust/components/containers/tab_list.rs b/rust/perspective-viewer/src/rust/components/containers/tab_list.rs index c1197d4f7b..5b5600f989 100644 --- a/rust/perspective-viewer/src/rust/components/containers/tab_list.rs +++ b/rust/perspective-viewer/src/rust/components/containers/tab_list.rs @@ -95,7 +95,7 @@ impl Component for TabList {
-
+
{ ctx.props().children.iter().nth(self.selected_idx) }
diff --git a/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs b/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs new file mode 100644 index 0000000000..15a8ec7f15 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs @@ -0,0 +1,55 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use yew::*; + +use crate::clone; + +#[derive(Properties)] +pub struct TrapDoorPanelProps { + pub id: Option<&'static str>, + pub class: Option<&'static str>, + pub children: Children, +} + +impl PartialEq for TrapDoorPanelProps { + fn eq(&self, _other: &Self) -> bool { + false + } +} + +/// A simple panel with an invisible inner `
` which stretches to fit the +/// width of the container, but will not shrink (unless the state is reset). +#[function_component(TrapDoorPanel)] +pub fn trap_door_panel(props: &TrapDoorPanelProps) -> Html { + let sizer = use_node_ref(); + let width = use_state_eq(|| 0.0); + use_effect({ + clone!(width, sizer); + move || { + width.set( + sizer + .cast::() + .unwrap() + .get_bounding_client_rect() + .width(), + ) + } + }); + + html! { +
+ { props.children.clone() } +
+
+ } +} diff --git a/rust/perspective-viewer/src/rust/components/form/debug.rs b/rust/perspective-viewer/src/rust/components/form/debug.rs new file mode 100644 index 0000000000..61390a52f0 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/form/debug.rs @@ -0,0 +1,237 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use std::rc::Rc; + +use yew::prelude::*; + +use self::js::PerspectiveValidationError; +use crate::components::containers::trap_door_panel::TrapDoorPanel; +use crate::components::form::code_editor::CodeEditor; +use crate::components::style::LocalStyle; +use crate::js::{copy_to_clipboard, paste_from_clipboard, MimeType}; +use crate::model::*; +use crate::presentation::*; +use crate::renderer::*; +use crate::session::*; +use crate::utils::*; +use crate::*; + +#[derive(Properties, Clone, PartialEq)] +pub struct DebugPanelProps { + pub session: Session, + pub renderer: Renderer, + pub presentation: Presentation, +} + +derive_model!(Presentation, Renderer, Session for DebugPanelProps); + +impl DebugPanelProps { + fn set_text(&self, setter: UseStateSetter>) { + let props = self.clone(); + ApiFuture::spawn(async move { + let task = props.get_viewer_config(); + let config = task.await?; + let json = JsValue::from_serde_ext(&config)?; + let js_string = + js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?; + + setter.set(Rc::new(js_string.as_string().unwrap())); + Ok(()) + }); + } + + fn reset_callback( + &self, + text: UseStateSetter>, + error: UseStateSetter>, + modified: UseStateSetter, + ) -> impl Fn(()) { + let props = self.clone(); + move |_| { + error.set(None); + props.set_text(text.clone()); + modified.set(false); + } + } +} + +fn on_save( + props: &DebugPanelProps, + text: &Rc, + error: &UseStateHandle>, + modified: &UseStateHandle, +) { + clone!(props, text, error, modified); + ApiFuture::spawn(async move { + match serde_json::from_str(&text) { + Ok(config) => { + match props.restore_and_render(config, async { Ok(()) }).await { + Ok(_) => { + modified.set(false); + }, + Err(e) => { + modified.set(true); + error.set(Some(PerspectiveValidationError { + error_message: JsValue::from(e) + .as_string() + .unwrap_or_else(|| "Failed to validate viewer config".to_owned()), + line: 0_i32, + column: 0, + })); + }, + } + Ok(()) + }, + Err(err) => { + modified.set(true); + error.set(Some(PerspectiveValidationError { + error_message: err.to_string(), + line: err.line() as i32 - 1, + column: err.column() as i32 - 1, + })); + + Ok(()) + }, + } + }); +} + +#[function_component(DebugPanel)] +pub fn debug_panel(props: &DebugPanelProps) -> Html { + let expr = use_state_eq(|| Rc::new("".to_string())); + let error = use_state_eq(|| Option::::None); + let select_all = use_memo((), |()| PubSub::default()); + let modified = use_state_eq(|| false); + use_effect_with((expr.setter(), props.clone()), { + clone!(error, modified); + move |(text, props)| { + props.set_text(text.clone()); + error.set(None); + let sub1 = props.renderer().style_changed.add_listener({ + props.reset_callback(text.clone(), error.setter(), modified.setter()) + }); + + let sub2 = props.renderer().reset_changed.add_listener({ + props.reset_callback(text.clone(), error.setter(), modified.setter()) + }); + + let sub3 = props.session().view_config_changed.add_listener({ + props.reset_callback(text.clone(), error.setter(), modified.setter()) + }); + + || { + drop(sub1); + drop(sub2); + drop(sub3); + } + } + }); + + let oninput = use_callback(expr.setter(), { + clone!(modified); + move |x, expr| { + modified.set(true); + expr.set(x) + } + }); + + let onsave = use_callback((expr.clone(), error.clone(), props.clone()), { + clone!(modified); + move |_, (text, error, props)| on_save(props, text, error, &modified) + }); + + let oncopy = use_callback( + (expr.clone(), select_all.callback()), + move |_, (text, select_all)| { + select_all.emit(()); + let mut options = web_sys::BlobPropertyBag::new(); + options.type_("text/plain"); + let blob_txt = (JsValue::from((***text).clone())).clone(); + let blob_parts = js_sys::Array::from_iter([blob_txt].iter()); + let blob = web_sys::Blob::new_with_str_sequence_and_options(&blob_parts, &options); + ApiFuture::spawn(copy_to_clipboard( + async move { Ok(blob?) }, + MimeType::TextPlain, + )); + }, + ); + + let onapply = use_callback((expr.clone(), error.clone(), props.clone()), { + clone!(modified); + move |_, (text, error, props)| on_save(props, text, error, &modified) + }); + + let onreset = use_callback((expr.setter(), error.clone(), props.clone()), { + clone!(modified); + move |_, (text, error, props)| { + props.set_text(text.clone()); + error.set(None); + modified.set(false); + } + }); + + let onpaste = use_callback((expr.clone(), error.clone(), props.clone()), { + clone!(modified); + move |_, (text, error, props)| { + clone!(text, error, props, modified); + ApiFuture::spawn(async move { + if let Some(x) = paste_from_clipboard().await { + let x = Rc::new(x); + modified.set(true); + error.set(None); + text.set(x.clone()); + on_save(&props, &x, &error, &modified); + } + + Ok(()) + }); + } + }); + + html! { + <> + + +
+ +
+ +
+
+ + +
+
+ +
+
+ +
+
+ + + + +
+ +
+ + } +} diff --git a/rust/perspective-viewer/src/rust/components/form/mod.rs b/rust/perspective-viewer/src/rust/components/form/mod.rs index 1e5c7bd6c2..cdf35a7177 100644 --- a/rust/perspective-viewer/src/rust/components/form/mod.rs +++ b/rust/perspective-viewer/src/rust/components/form/mod.rs @@ -18,6 +18,7 @@ pub mod code_editor; pub mod color_range_selector; pub mod color_selector; +pub mod debug; mod highlight; pub mod number_field; pub mod number_input; diff --git a/rust/perspective-viewer/src/rust/components/viewer.rs b/rust/perspective-viewer/src/rust/components/viewer.rs index 8d76328978..e7bb4fa0be 100644 --- a/rust/perspective-viewer/src/rust/components/viewer.rs +++ b/rust/perspective-viewer/src/rust/components/viewer.rs @@ -19,6 +19,7 @@ use yew::prelude::*; use super::column_selector::ColumnSelector; use super::containers::split_panel::SplitPanel; use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus}; +use super::form::debug::DebugPanel; use super::plugin_selector::PluginSelector; use super::render_warning::RenderWarning; use super::status_bar::StatusBar; @@ -125,6 +126,7 @@ pub enum PerspectiveViewerMsg { Reset(bool, Option>), ToggleSettingsInit(Option, Option>>), ToggleSettingsComplete(SettingsUpdate, Sender<()>), + ToggleDebug, PreloadFontsUpdate, RenderLimits(Option<(usize, usize, Option, Option)>), SettingsPanelSizeUpdate(Option), @@ -141,6 +143,7 @@ pub struct PerspectiveViewer { on_rendered: Option>, fonts: FontLoaderProps, settings_open: bool, + debug_open: bool, /// The column which will be opened in the ColumnSettingsSidebar selected_column: Option, selected_column_is_active: bool, // TODO: should we use a struct? @@ -217,6 +220,7 @@ impl Component for PerspectiveViewer { on_rendered: None, fonts: FontLoaderProps::new(&elem, callback), settings_open: false, + debug_open: false, selected_column: None, selected_column_is_active: false, on_resize: Default::default(), @@ -266,6 +270,15 @@ impl Component for PerspectiveViewer { needs_update }, + PerspectiveViewerMsg::ToggleDebug => { + self.debug_open = !self.debug_open; + clone!(ctx.props().renderer, ctx.props().session); + ApiFuture::spawn(async move { + renderer.draw(session.validate().await?.create_view()).await + }); + + true + }, PerspectiveViewerMsg::ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false, PerspectiveViewerMsg::ToggleSettingsInit( Some(SettingsUpdate::Missing), @@ -335,8 +348,8 @@ impl Component for PerspectiveViewer { .as_ref() .map(|l| l.is_active(&ctx.props().session)) .unwrap_or_default(); - self.selected_column_is_active = is_active; + self.selected_column_is_active = is_active; if toggle && self.selected_column == locator { self.selected_column = None; (false, None) @@ -415,6 +428,7 @@ impl Component for PerspectiveViewer { .link() .callback(|()| PerspectiveViewerMsg::ToggleSettingsInit(None, None)); + let on_toggle_debug = ctx.link().callback(|_| PerspectiveViewerMsg::ToggleDebug); let mut class = classes!("settings-closed"); if ctx.props().is_title() { class.push("titled"); @@ -448,6 +462,10 @@ impl Component for PerspectiveViewer { on_close_sidebar={&on_close_settings} /> } + if self.settings_open { - - { settings_panel } - { main_panel } - + if self.debug_open { + + + { settings_panel } + { main_panel } + + } else { + + { settings_panel } + { main_panel } + + } } else { ApiFuture<()> { tracing::info!("Restoring ViewerConfig"); global::document().blur_active_element(); - clone!(self.session, self.renderer, self.root, self.presentation); + let this = self.clone(); ApiFuture::new(async move { let decoded_update = ViewerConfigUpdate::decode(&update)?; - - let ViewerConfigUpdate { - plugin, - plugin_config, - columns_config, - settings, - theme: theme_name, - title, - mut view_config, - ..//version - } = decoded_update; - - if !session.has_table() { - if let OptionalUpdate::Update(x) = settings { - presentation.set_settings_attribute(x); - } - } - - if let OptionalUpdate::Update(title) = title { - presentation.set_title(Some(title)); - } else if matches!(title, OptionalUpdate::SetDefault) { - presentation.set_title(None); - } - - let needs_restyle = match theme_name { - OptionalUpdate::SetDefault => { - let current_name = presentation.get_selected_theme_name().await; - if current_name.is_some() { - presentation.set_theme_name(None).await?; - true - } else { - false - } - }, - OptionalUpdate::Update(x) => { - let current_name = presentation.get_selected_theme_name().await; - if current_name.is_some() && current_name.as_ref().unwrap() != &x { - presentation.set_theme_name(Some(&x)).await?; - true - } else { - false - } - }, - _ => false, - }; - - let plugin_changed = renderer.update_plugin(&plugin)?; - if plugin_changed { - session.set_update_column_defaults(&mut view_config, &renderer.metadata()); - } - - session.update_view_config(view_config); - let draw_task = renderer.draw(async { - let task = root + let settings = decoded_update.settings.clone(); + let root = this.root.clone(); + this.restore_and_render(decoded_update, async move { + let result = root .borrow() .as_ref() - .ok_or("Already deleted")? + .into_apierror()? .send_message_async(move |x| { PerspectiveViewerMsg::ToggleSettingsComplete(settings, x) }); - let internal_task = async { - let plugin = renderer.get_active_plugin()?; - let plugin_update = if let Some(x) = plugin_config { - JsValue::from_serde_ext(&x).unwrap() - } else { - plugin.save() - }; - presentation.update_columns_configs(columns_config); - let columns_config = presentation.all_columns_configs(); - plugin.restore(&plugin_update, Some(&columns_config)); - session.validate().await?.create_view().await - } - .await; - - task.await?; - internal_task - }); - - draw_task.await?; - - // TODO this should be part of the API for `draw()` above, such that - // the plugin need not render twice when a theme is provided. - if needs_restyle { - let view = session.get_view().into_apierror()?; - renderer.restyle_all(&view).await?; - } - + Ok(result.await?) + }) + .await?; Ok(()) }) } diff --git a/rust/perspective-viewer/src/rust/js/clipboard.rs b/rust/perspective-viewer/src/rust/js/clipboard.rs index cf6f526571..5dff13f08a 100644 --- a/rust/perspective-viewer/src/rust/js/clipboard.rs +++ b/rust/perspective-viewer/src/rust/js/clipboard.rs @@ -16,12 +16,20 @@ use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; use super::mimetype::*; use crate::js::clipboard_item::*; use crate::utils::*; use crate::*; +pub async fn paste_from_clipboard() -> Option { + JsFuture::from(global::clipboard().read_text()) + .await + .ok() + .and_then(|x| x.as_string()) +} + /// Copy a `JsPerspectiveView` to the clipboard as a CSV. pub fn copy_to_clipboard( view: impl Future>, diff --git a/rust/perspective-viewer/src/rust/model/mod.rs b/rust/perspective-viewer/src/rust/model/mod.rs index 59b2a3ea30..81f3c4652c 100644 --- a/rust/perspective-viewer/src/rust/model/mod.rs +++ b/rust/perspective-viewer/src/rust/model/mod.rs @@ -75,6 +75,7 @@ mod intersection_observer; mod is_invalid_drop; mod plugin_column_styles; mod resize_observer; +mod restore_and_render; mod structural; mod update_and_render; @@ -87,5 +88,6 @@ pub use self::intersection_observer::*; pub use self::is_invalid_drop::*; pub use self::plugin_column_styles::*; pub use self::resize_observer::*; +pub use self::restore_and_render::*; pub use self::structural::*; pub use self::update_and_render::*; diff --git a/rust/perspective-viewer/src/rust/model/restore_and_render.rs b/rust/perspective-viewer/src/rust/model/restore_and_render.rs new file mode 100644 index 0000000000..388deb91d9 --- /dev/null +++ b/rust/perspective-viewer/src/rust/model/restore_and_render.rs @@ -0,0 +1,107 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use futures::Future; + +use super::structural::*; +use crate::config::{OptionalUpdate, ViewerConfigUpdate}; +use crate::utils::*; +use crate::*; + +pub trait RestoreAndRender: HasRenderer + HasSession + HasPresentation { + /// Apply a `ViewConfigUpdate` to the current `View` and render. + fn restore_and_render( + &self, + ViewerConfigUpdate { + plugin, + plugin_config, + columns_config, + settings, + theme: theme_name, + title, + mut view_config, + .. + }: crate::config::ViewerConfigUpdate, + task: impl Future> + 'static, + ) -> ApiFuture<()> { + clone!(self.session(), self.renderer(), self.presentation()); + ApiFuture::new(async move { + if !session.has_table() { + if let OptionalUpdate::Update(x) = settings { + presentation.set_settings_attribute(x); + } + } + + if let OptionalUpdate::Update(title) = title { + presentation.set_title(Some(title)); + } else if matches!(title, OptionalUpdate::SetDefault) { + presentation.set_title(None); + } + + let needs_restyle = match theme_name { + OptionalUpdate::SetDefault => { + let current_name = presentation.get_selected_theme_name().await; + if current_name.is_some() { + presentation.set_theme_name(None).await?; + true + } else { + false + } + }, + OptionalUpdate::Update(x) => { + let current_name = presentation.get_selected_theme_name().await; + if current_name.is_some() && current_name.as_ref().unwrap() != &x { + presentation.set_theme_name(Some(&x)).await?; + true + } else { + false + } + }, + _ => false, + }; + + let plugin_changed = renderer.update_plugin(&plugin)?; + if plugin_changed { + session.set_update_column_defaults(&mut view_config, &renderer.metadata()); + } + + session.update_view_config(view_config); + let draw_task = renderer.draw(async { + task.await?; + let plugin = renderer.get_active_plugin()?; + let plugin_update = if let Some(x) = plugin_config { + JsValue::from_serde_ext(&x).unwrap() + } else { + plugin.save() + }; + + presentation.update_columns_configs(columns_config); + let columns_config = presentation.all_columns_configs(); + plugin.restore(&plugin_update, Some(&columns_config)); + session.validate().await?.create_view().await + }); + + draw_task.await?; + + // TODO this should be part of the API for `draw()` above, such that + // the plugin need not render twice when a theme is provided. + if needs_restyle { + let view = session.get_view().into_apierror()?; + renderer.restyle_all(&view).await?; + } + + Ok(()) + }) + } +} + +impl RestoreAndRender for T {} diff --git a/rust/perspective-viewer/src/rust/session.rs b/rust/perspective-viewer/src/rust/session.rs index a2bbf0374d..694444f85b 100644 --- a/rust/perspective-viewer/src/rust/session.rs +++ b/rust/perspective-viewer/src/rust/session.rs @@ -181,13 +181,18 @@ impl Session { self.borrow().config.is_column_expression_in_use(name) } + /// Is this column currently being used or not pub fn is_column_active(&self, name: &str) -> bool { - self.borrow().config.columns.iter().any(|maybe_col| { + let config = Ref::map(self.borrow(), |x| &x.config); + config.columns.iter().any(|maybe_col| { maybe_col .as_ref() .map(|col| col == name) .unwrap_or_default() - }) + }) || config.group_by.iter().any(|col| col == name) + || config.split_by.iter().any(|col| col == name) + || config.filter.iter().any(|col| col.0 == name) + || config.sort.iter().any(|col| col.0 == name) } pub fn create_drag_drop_update( diff --git a/rust/perspective-viewer/src/rust/utils/browser/selection.rs b/rust/perspective-viewer/src/rust/utils/browser/selection.rs index 50c1c59b43..f4d3e2b878 100644 --- a/rust/perspective-viewer/src/rust/utils/browser/selection.rs +++ b/rust/perspective-viewer/src/rust/utils/browser/selection.rs @@ -19,11 +19,22 @@ use crate::*; /// but `Deref` makes them fall through, so it is important that this method /// be called on the correct struct type! pub trait CaretPosition { + fn select_all(&self) -> ApiResult<()>; fn set_caret_position(&self, offset: usize) -> ApiResult<()>; fn get_caret_position(&self) -> Option; } impl CaretPosition for web_sys::HtmlElement { + fn select_all(&self) -> ApiResult<()> { + let range = global::document().create_range()?; + let selection = global::window().get_selection()?.into_apierror()?; + range.set_start(self, 0_u32)?; + range.set_end(self, 10000000_u32)?; + selection.remove_all_ranges()?; + selection.add_range(&range)?; + Ok(()) + } + fn set_caret_position(&self, offset: usize) -> ApiResult<()> { let range = global::document().create_range()?; let selection = global::window().get_selection()?.into_apierror()?; @@ -50,8 +61,13 @@ impl CaretPosition for web_sys::HtmlElement { } impl CaretPosition for web_sys::HtmlTextAreaElement { + fn select_all(&self) -> ApiResult<()> { + self.set_selection_start(Some(0_u32))?; + self.set_selection_end(Some(1000000000_u32))?; + Ok(()) + } + fn set_caret_position(&self, offset: usize) -> ApiResult<()> { - self.focus().unwrap(); self.set_selection_end(Some(offset as u32))?; self.set_selection_start(Some(offset as u32))?; Ok(()) diff --git a/rust/perspective-viewer/src/svg/drawer-tab-invert-hover.svg b/rust/perspective-viewer/src/svg/drawer-tab-invert-hover.svg new file mode 100644 index 0000000000..d950b6f6c9 --- /dev/null +++ b/rust/perspective-viewer/src/svg/drawer-tab-invert-hover.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/rust/perspective-viewer/src/svg/drawer-tab-invert.svg b/rust/perspective-viewer/src/svg/drawer-tab-invert.svg new file mode 100644 index 0000000000..5f6026db18 --- /dev/null +++ b/rust/perspective-viewer/src/svg/drawer-tab-invert.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/rust/perspective-viewer/src/themes/intl.less b/rust/perspective-viewer/src/themes/intl.less index 8a2d83c8ec..6c88aa7bf1 100644 --- a/rust/perspective-viewer/src/themes/intl.less +++ b/rust/perspective-viewer/src/themes/intl.less @@ -94,4 +94,5 @@ perspective-dropdown { // Tabs --style-tab-label--content: "Style"; --attributes-tab-label--content: "Attributes"; + --debug-tab-label--content: "Debug JSON"; } diff --git a/rust/perspective-viewer/src/themes/intl/de.less b/rust/perspective-viewer/src/themes/intl/de.less index dc55fe0687..a644d8cee5 100644 --- a/rust/perspective-viewer/src/themes/intl/de.less +++ b/rust/perspective-viewer/src/themes/intl/de.less @@ -94,4 +94,5 @@ perspective-dropdown { // Tabs --style-tab-label--content: "Stil"; --attributes-tab-label--content: "Attribute"; + --debug-tab-label--content: "Debug JSON"; } diff --git a/rust/perspective-viewer/src/themes/intl/es.less b/rust/perspective-viewer/src/themes/intl/es.less index 1c92088ce2..6ad041837a 100644 --- a/rust/perspective-viewer/src/themes/intl/es.less +++ b/rust/perspective-viewer/src/themes/intl/es.less @@ -94,4 +94,5 @@ perspective-dropdown { // Tabs --style-tab-label--content: "Estilo"; --attributes-tab-label--content: "Atributos"; + --debug-tab-label--content: "Debug JSON"; } diff --git a/rust/perspective-viewer/src/themes/intl/fr.less b/rust/perspective-viewer/src/themes/intl/fr.less index 36f5472716..0457c23616 100644 --- a/rust/perspective-viewer/src/themes/intl/fr.less +++ b/rust/perspective-viewer/src/themes/intl/fr.less @@ -94,4 +94,5 @@ perspective-dropdown { // Tabs --style-tab-label--content: "Style"; --attributes-tab-label--content: "Les attributs"; + --debug-tab-label--content: "Debug JSON"; } diff --git a/rust/perspective-viewer/src/themes/intl/ja.less b/rust/perspective-viewer/src/themes/intl/ja.less index 51024d8310..ab5868e622 100644 --- a/rust/perspective-viewer/src/themes/intl/ja.less +++ b/rust/perspective-viewer/src/themes/intl/ja.less @@ -94,4 +94,5 @@ perspective-dropdown { // Tabs --style-tab-label--content: "スタイル"; --attributes-tab-label--content: "属性"; + --debug-tab-label--content: "Debug JSON"; } diff --git a/rust/perspective-viewer/src/themes/intl/pt.less b/rust/perspective-viewer/src/themes/intl/pt.less index 3f1ee25210..8c0abf287d 100644 --- a/rust/perspective-viewer/src/themes/intl/pt.less +++ b/rust/perspective-viewer/src/themes/intl/pt.less @@ -94,4 +94,5 @@ perspective-dropdown { // Tabs --style-tab-label--content: "Estilo"; --attributes-tab-label--content: "Atributos"; + --debug-tab-label--content: "Debug JSON"; } diff --git a/rust/perspective-viewer/src/themes/intl/zh.less b/rust/perspective-viewer/src/themes/intl/zh.less index 2030f17bed..014a7609f2 100644 --- a/rust/perspective-viewer/src/themes/intl/zh.less +++ b/rust/perspective-viewer/src/themes/intl/zh.less @@ -94,4 +94,5 @@ perspective-dropdown { // Tabs --style-tab-label--content: "风格"; --attributes-tab-label--content: "属性"; + --debug-tab-label--content: "Debug JSON"; }