Skip to content

Commit d0a1823

Browse files
committed
Add filename option, JSON and Config formats to export menu
1 parent 8cd9fa5 commit d0a1823

File tree

15 files changed

+396
-296
lines changed

15 files changed

+396
-296
lines changed

packages/perspective-viewer-d3fc/src/js/plugin/plugin.js

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -225,26 +225,10 @@ export function register(...plugins) {
225225
);
226226
}
227227

228-
const button = document.createElement("a");
229-
button.setAttribute("download", "perspective.png");
230-
document.body.appendChild(button);
231-
button.addEventListener(
232-
"click",
233-
function dlCanvas() {
234-
var dt = canvas.toDataURL("image/png"); // << this fails in IE/Edge...
235-
dt = dt.replace(
236-
/^data:image\/[^;]*/,
237-
"data:application/octet-stream"
238-
);
239-
dt = dt.replace(
240-
/^data:application\/octet-stream/,
241-
"data:application/octet-stream;headers=Content-Disposition%3A%20attachment%3B%20filename=perspective.png"
242-
);
243-
this.href = dt;
244-
},
245-
false
228+
return await new Promise(
229+
(x) => canvas.toBlob((blob) => x(blob)),
230+
"image/png"
246231
);
247-
button.click();
248232
}
249233

250234
async draw(view, end_col, end_row) {

rust/perspective-viewer/src/less/dropdown-menu.less

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,26 @@
2929
monospace;
3030
}
3131

32+
input {
33+
margin-left: 12px;
34+
margin-right: 12px;
35+
padding: 0;
36+
border: none;
37+
border-bottom: 1px solid var(--inactive--color, #ccc);
38+
background: transparent;
39+
font-family: var(--interface-monospace--font-family, "Roboto Mono"),
40+
monospace;
41+
font-weight: 300;
42+
font-size: 14px;
43+
color: inherit;
44+
outline: none;
45+
}
46+
47+
.invalid {
48+
color: var(--error--color, #ff0000);
49+
border-color: var(--error--color, #ff0000);
50+
}
51+
3252
.selected {
3353
background-color: rgba(0, 0, 0, 0.05);
3454
}
@@ -41,6 +61,7 @@
4161
display: flex;
4262
flex-direction: column;
4363
margin-left: 12px;
64+
margin-right: 12px;
4465
}
4566

4667
span {

rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ pub struct DropDownMenu<T>
3434
where
3535
T: Into<Html> + Clone + PartialEq + 'static,
3636
{
37-
top: i32,
38-
left: i32,
37+
position: Option<(i32, i32)>,
3938
_props: PhantomData<T>,
4039
}
4140

@@ -48,25 +47,20 @@ where
4847

4948
fn create(_ctx: &Context<Self>) -> Self {
5049
DropDownMenu {
51-
top: 0,
52-
left: 0,
50+
position: None,
5351
_props: Default::default(),
5452
}
5553
}
5654

5755
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
5856
match msg {
5957
DropDownMenuMsg::SetPos(top, left) => {
60-
self.top = top;
61-
self.left = left;
58+
self.position = Some((top, left));
6259
true
6360
}
6461
}
6562
}
6663

67-
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
68-
false
69-
}
7064

7165
fn view(&self, ctx: &Context<Self>) -> Html {
7266
let values = &ctx.props().values;
@@ -118,8 +112,14 @@ where
118112
<>
119113
<style>
120114
{ &CSS }
121-
{ format!(":host{{left:{}px;top:{}px;}}", self.left, self.top) }
122115
</style>
116+
{
117+
self.position.map(|(top, left)| html! {
118+
<style>
119+
{ format!(":host{{left:{}px;top:{}px;}}", left, top) }
120+
</style>
121+
}).unwrap_or_default()
122+
}
123123
{ body }
124124
</>
125125
}

rust/perspective-viewer/src/rust/components/export_dropdown.rs

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,121 @@
77
// file.
88

99
use super::containers::dropdown_menu::*;
10+
use crate::model::*;
11+
use crate::renderer::*;
12+
1013
use crate::*;
14+
use js_intern::*;
15+
use std::rc::Rc;
1116
use yew::prelude::*;
1217

13-
#[derive(Clone, Copy, PartialEq)]
14-
pub enum ExportMethod {
15-
Csv,
16-
CsvAll,
17-
Html,
18-
Png,
19-
Arrow,
20-
ArrowAll,
18+
pub type ExportDropDownMenuItem = DropDownMenuItem<ExportFile>;
19+
20+
#[derive(Properties, Clone, PartialEq)]
21+
pub struct ExportDropDownMenuProps {
22+
pub renderer: Renderer,
23+
pub callback: Callback<ExportFile>,
2124
}
2225

23-
impl From<ExportMethod> for Html {
24-
fn from(x: ExportMethod) -> Self {
25-
match x {
26-
ExportMethod::Csv => html_template! {
27-
<code>{ ".csv " }</code>
28-
},
29-
ExportMethod::CsvAll => html_template! {
30-
<code>{ ".csv " }</code>
31-
},
32-
ExportMethod::Html => html_template! {
33-
<code>{ ".html " }</code>
34-
},
35-
ExportMethod::Png => html_template! {
36-
<code>{ ".png " }</code>
37-
},
38-
ExportMethod::Arrow => html_template! {
39-
<code>{ ".arrow " }</code>
40-
},
41-
ExportMethod::ArrowAll => html_template! {
42-
<code>{ ".arrow " }</code>
26+
#[derive(Default)]
27+
pub struct ExportDropDownMenu {
28+
top: i32,
29+
left: i32,
30+
title: String,
31+
input_ref: NodeRef,
32+
invalid: bool,
33+
}
34+
35+
pub enum ExportDropDownMenuMsg {
36+
SetPos(i32, i32),
37+
TitleChange,
38+
}
39+
40+
fn get_menu_items(name: &str, has_render: bool) -> Vec<ExportDropDownMenuItem> {
41+
vec![
42+
ExportDropDownMenuItem::OptGroup(
43+
"Current View",
44+
if has_render {
45+
vec![
46+
ExportMethod::Csv.new_file(name),
47+
ExportMethod::Json.new_file(name),
48+
ExportMethod::Arrow.new_file(name),
49+
ExportMethod::Html.new_file(name),
50+
ExportMethod::Png.new_file(name),
51+
]
52+
} else {
53+
vec![
54+
ExportMethod::Csv.new_file(name),
55+
ExportMethod::Json.new_file(name),
56+
ExportMethod::Arrow.new_file(name),
57+
ExportMethod::Html.new_file(name),
58+
]
4359
},
60+
),
61+
ExportDropDownMenuItem::OptGroup(
62+
"All",
63+
vec![
64+
ExportMethod::CsvAll.new_file(name),
65+
ExportMethod::JsonAll.new_file(name),
66+
ExportMethod::ArrowAll.new_file(name),
67+
],
68+
),
69+
ExportDropDownMenuItem::OptGroup(
70+
"Config",
71+
vec![ExportMethod::JsonConfig.new_file(name)],
72+
),
73+
]
74+
}
75+
76+
impl Component for ExportDropDownMenu {
77+
type Properties = ExportDropDownMenuProps;
78+
type Message = ExportDropDownMenuMsg;
79+
80+
fn view(&self, ctx: &Context<Self>) -> yew::virtual_dom::VNode {
81+
let callback = ctx.link().callback(|_| ExportDropDownMenuMsg::TitleChange);
82+
let plugin = ctx.props().renderer.get_active_plugin().unwrap();
83+
let has_render = js_sys::Reflect::has(&plugin, js_intern!("render")).unwrap();
84+
html_template! {
85+
<style>
86+
{ format!(":host{{left:{}px;top:{}px;}}", self.left, self.top) }
87+
</style>
88+
<span class="dropdown-group-label">{ "Save as" }</span>
89+
<input
90+
class={ if self.invalid { "invalid" } else { "" }}
91+
oninput={ callback }
92+
ref={ self.input_ref.clone() }
93+
value={ self.title.to_owned() } />
94+
<DropDownMenu<ExportFile>
95+
values={ Rc::new(get_menu_items(&self.title, has_render)) }
96+
callback={ ctx.props().callback.clone() }>
97+
</DropDownMenu<ExportFile>>
4498
}
4599
}
46-
}
47100

48-
pub type ExportDropDownMenu = DropDownMenu<ExportMethod>;
49-
pub type ExportDropDownMenuProps = DropDownMenuProps<ExportMethod>;
50-
pub type ExportDropDownMenuMsg = DropDownMenuMsg;
51-
pub type ExportDropDownMenuItem = DropDownMenuItem<ExportMethod>;
101+
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
102+
match msg {
103+
ExportDropDownMenuMsg::SetPos(top, left) => {
104+
self.top = top;
105+
self.left = left;
106+
true
107+
}
108+
ExportDropDownMenuMsg::TitleChange => {
109+
self.title = self
110+
.input_ref
111+
.cast::<web_sys::HtmlInputElement>()
112+
.unwrap()
113+
.value();
114+
115+
self.invalid = self.title.is_empty();
116+
true
117+
}
118+
}
119+
}
120+
121+
fn create(_ctx: &Context<Self>) -> Self {
122+
ExportDropDownMenu {
123+
title: "untitled".to_owned(),
124+
..Default::default()
125+
}
126+
}
127+
}

rust/perspective-viewer/src/rust/config.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ pub enum ViewerConfigEncoding {
4242

4343
#[serde(rename = "arraybuffer")]
4444
ArrayBuffer,
45+
46+
#[serde(skip_serializing)]
47+
JSONString,
4548
}
4649

4750
#[derive(Serialize, PartialEq)]
@@ -90,8 +93,11 @@ impl ViewerConfig {
9093
.slice_with_end(start, start + len)
9194
.unchecked_into())
9295
}
96+
Some(ViewerConfigEncoding::JSONString) => {
97+
Ok(JsValue::from(serde_json::to_string(self).into_jserror()?))
98+
}
9399
None | Some(ViewerConfigEncoding::JSON) => {
94-
Ok(JsValue::from_serde(self).unwrap())
100+
JsValue::from_serde(self).into_jserror()
95101
}
96102
}
97103
}

0 commit comments

Comments
 (0)