mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-29 18:22:38 +01:00
webui
This commit is contained in:
+1
-8
@@ -28,14 +28,7 @@ wasm-bindgen-futures = "0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Document",
|
||||
"Element",
|
||||
"Node",
|
||||
"Window",
|
||||
"HtmlCollection",
|
||||
"HtmlInputElement",
|
||||
]
|
||||
features = ["Document", "Element", "Node", "Window", "HtmlCollection", "HtmlInputElement", "DomRect"]
|
||||
|
||||
[dev-dependencies]
|
||||
js-sys = "0.3"
|
||||
|
||||
@@ -92,8 +92,8 @@
|
||||
"XTREAM_CODES": "xtream codes",
|
||||
"XC": "xc",
|
||||
"M3U": "m3u",
|
||||
"XTREAM": "Xtream",
|
||||
"HDHOMERUN": "HdHomerun",
|
||||
"XTREAM": "xtream",
|
||||
"HDHOMERUN": "hdHomerun",
|
||||
"STRM": "strm",
|
||||
"PLAYLIST_BROWSER": "Playlist Browser",
|
||||
"RATE_LIMIT": "Rate Limit",
|
||||
@@ -144,7 +144,9 @@
|
||||
"DELAY": "Delay",
|
||||
"FILENAME": "Filename",
|
||||
"INCLUDE_TYPE_IN_URL": "Include type in url",
|
||||
"MASK_REDIRECT_URL": "Mask redirect url"
|
||||
"MASK_REDIRECT_URL": "Mask redirect url",
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete"
|
||||
},
|
||||
"TABLE": {
|
||||
"EMPTY": "",
|
||||
|
||||
@@ -346,5 +346,9 @@
|
||||
{
|
||||
"keys": ["Ellipsis", "Show"],
|
||||
"path": "M 4.5000013,9 C 3.1249997,9 2,10.125024 2,11.500015 c 0,1.374992 1.1249997,2.499969 2.5000013,2.499969 1.3750012,0 2.500001,-1.124977 2.500001,-2.499969 C 7.0000023,10.125024 5.8750025,9 4.5000013,9 M 19.499999,9 c -1.375001,0 -2.500001,1.125024 -2.500001,2.500015 0,1.374992 1.125,2.499969 2.500001,2.499969 C 20.875,13.999984 22,12.875007 22,11.500015 22,10.125024 20.875,9 19.499999,9 M 12,9 c -1.375001,0 -2.5000012,1.125024 -2.5000012,2.500015 0,1.374992 1.1250002,2.499969 2.5000012,2.499969 1.375001,0 2.500001,-1.124977 2.500001,-2.499969 C 14.500001,10.125024 13.375001,9 12,9"
|
||||
},
|
||||
{
|
||||
"keys": ["Hamburger", "Menu", "Popup", "Bars"],
|
||||
"path": "M 2,18.333002 H 21.9995 V 16.110835 H 2 Z M 2,12.777585 H 21.9995 V 10.555417 H 2 Z M 2,5 V 7.2221668 H 21.9995 V 5 Z"
|
||||
}
|
||||
]
|
||||
@@ -63,8 +63,7 @@ dialog {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.tp__rotated_text {
|
||||
writing-mode: vertical-rl; // or vertical-lr
|
||||
text-orientation: upright;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.tp__delete_action {
|
||||
color: var(--attention-color) !important;
|
||||
fill: var(--attention-color) !important;
|
||||
}
|
||||
+12
-9
@@ -74,6 +74,18 @@
|
||||
--tag-inactive-background-color: #3a3a3a;
|
||||
|
||||
|
||||
--popup-background-color: #18181b;
|
||||
--popup-shadow: 0 3px 8px #06070a;
|
||||
--popup-border-color: #2f2f31;
|
||||
--popup-color: #fff;
|
||||
//--popup-hover-background-color: #5599f0;
|
||||
//--popup-hover-color: #fff;
|
||||
|
||||
--attention-color: #f67070;
|
||||
--warn-color: #b88e22;
|
||||
|
||||
|
||||
|
||||
--hint-color: #888888;
|
||||
--label-color: #73d6fe;
|
||||
|
||||
@@ -96,15 +108,6 @@
|
||||
--toolbar-button-hover-color: #ffffff;
|
||||
--toolbar-button-hover-background-color: #004db4;
|
||||
|
||||
--popup-background-color: #282A36;
|
||||
--popup-shadow-color: #06070a;
|
||||
--popup-border-color: #646464;
|
||||
--popup-color: #fff;
|
||||
--popup-hover-background-color: #5599f0;
|
||||
--popup-hover-color: #fff;
|
||||
|
||||
--attention-color: red;
|
||||
--warn-color: orange;
|
||||
--ok-color: #21e821;
|
||||
|
||||
--card-background-color-darker: #1c2026;
|
||||
|
||||
@@ -24,5 +24,7 @@
|
||||
@forward "components/playlist/target/m3u_output";
|
||||
@forward "components/playlist/target/strm_output";
|
||||
@forward "components/playlist/target/hdhomerun_output";
|
||||
@forward "components/playlist/target/target_table";
|
||||
@forward "components/tag_list";
|
||||
@forward "components/chip";
|
||||
@forward "components/chip";
|
||||
@forward "components/popup_menu";
|
||||
|
||||
@@ -25,4 +25,15 @@
|
||||
width: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
max-height: 5000px;
|
||||
// transition: max-height 0.5s ease;
|
||||
// max-height: 5000px;
|
||||
}
|
||||
&.tp__collapsed {
|
||||
.tp__collapse-panel__body {
|
||||
max-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
}
|
||||
label {
|
||||
pointer-events: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
.tp__popup-menu {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--popup-background-color);
|
||||
color: var(--popup-color);
|
||||
box-shadow: var(--popup-shadow);
|
||||
z-index: 9999;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
li {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
fill: var(--modest-text-color);
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
|
||||
@@ -1,29 +1,3 @@
|
||||
.tp__m3u_output {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-small);
|
||||
|
||||
&__section {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-small);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--padding-micro) var(--padding-mini);
|
||||
background-color: var(--table-card-background-color);
|
||||
|
||||
.tp__tag_list {
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 1.1rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&:after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,4 +2,36 @@
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-small);
|
||||
}
|
||||
|
||||
.tp__target_output__output {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-small);
|
||||
|
||||
&__section {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-small);
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--padding-micro) var(--padding-mini);
|
||||
background-color: var(--table-card-background-color);
|
||||
|
||||
.tp__tag_list {
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 1.1rem;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
|
||||
&:after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.tp__target-table {
|
||||
.tp__icon-button {
|
||||
width: 2rem;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,3 @@
|
||||
.tp__xtream_output {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-small);
|
||||
|
||||
&__section {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--gap-small);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--padding-micro) var(--padding-mini);
|
||||
background-color: var(--table-card-background-color);
|
||||
|
||||
.tp__tag_list {
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 1.1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,16 +19,14 @@ pub fn CollapsePanel(props: &CollapsePanelProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__collapse-panel">
|
||||
<div class={classes!("tp__collapse-panel", if *expanded {""} else {"tp__collapsed"})}>
|
||||
<div class="tp__collapse-panel__header" onclick={toggle}>
|
||||
<span>{ props.title.clone() }</span>
|
||||
<AppIcon name={ if *expanded { "ChevronUp" } else {"ChevronDown"} }/>
|
||||
</div>
|
||||
if *expanded {
|
||||
<div class="tp__collapse-panel__body">
|
||||
{ for props.children.iter() }
|
||||
</div>
|
||||
}
|
||||
<div class="tp__collapse-panel__body">
|
||||
{ for props.children.iter() }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ pub struct MenuItemProps {
|
||||
pub label: String,
|
||||
pub icon: String,
|
||||
#[prop_or_default]
|
||||
pub style: String,
|
||||
#[prop_or_default]
|
||||
pub onclick: Callback<String>,
|
||||
}
|
||||
|
||||
@@ -24,7 +26,7 @@ pub fn MenuItem(props: &MenuItemProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__menu-item" onclick={ handle_click }>
|
||||
<div class={classes!("tp__menu-item", props.style.to_string())} onclick={ handle_click }>
|
||||
<AppIcon name={props.icon.clone()}></AppIcon>
|
||||
<label>{props.label.clone()}</label>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ mod toggle_switch;
|
||||
mod reveal_content;
|
||||
mod tag_list;
|
||||
mod chip;
|
||||
mod popup_menu;
|
||||
|
||||
pub use self::svg_icon::*;
|
||||
// pub use self::input::*;
|
||||
@@ -42,3 +43,4 @@ pub use self::table::*;
|
||||
pub use self::toggle_switch::*;
|
||||
use self::chip::*;
|
||||
use self::tag_list::*;
|
||||
use self::popup_menu::*;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub struct HdHomeRunOutputProps {
|
||||
pub fn HdHomeRunOutput(props: &HdHomeRunOutputProps) -> Html {
|
||||
|
||||
html! {
|
||||
<div class="tp__hdhomerun_output">
|
||||
<div class="tp__hdhomerun_output tp__target_output__output">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,12 @@ pub fn M3uOutput(props: &M3uOutputProps) -> Html {
|
||||
|
||||
|
||||
html! {
|
||||
<div class="tp__m3u_output">
|
||||
<div class="tp__m3u_output__section">
|
||||
<span class="tp__m3u_output__label">{translator.t("LABEL.FILENAME")}</span>
|
||||
<span>{props.output.filename.as_ref().map_or_else(|| String::new(), |f| f.to_string())}</span>
|
||||
<div class="tp__m3u_output tp__target_output__output">
|
||||
<div class="tp__target_output__output__section">
|
||||
<span class="tp__target_output__output__label">{translator.t("LABEL.FILENAME")}</span>
|
||||
{ props.output.filename.as_ref().map(|f| html! {<span>{ f }</span>}) }
|
||||
</div>
|
||||
<div class="tp__m3u_output__section">
|
||||
<div class="tp__target_output__output__section">
|
||||
<TagList tags={(*tags).clone()} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ pub struct StrmOutputProps {
|
||||
pub fn StrmOutput(props: &StrmOutputProps) -> Html {
|
||||
|
||||
html! {
|
||||
<div class="tp__strm_output">
|
||||
<div class="tp__strm_output tp__target_output__output">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::model::XtreamTargetOutputDto;
|
||||
use crate::app::components::chip::{convert_bool_to_chip_style, Chip, Tag};
|
||||
use crate::app::components::chip::{convert_bool_to_chip_style, Tag};
|
||||
use crate::app::components::tag_list::{TagList};
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
@@ -39,16 +39,14 @@ pub fn XtreamOutput(props: &XtreamOutputProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__xtream_output">
|
||||
<div class="tp__xtream_output__section">
|
||||
<span class="tp__xtream_output__label">{translator.t("LABEL.SKIP_DIRECT_SOURCE")}</span>
|
||||
<div class="tp__xtream_output tp__target_output__output">
|
||||
<div class="tp__target_output__output__section">
|
||||
<span class="tp__target_output__output__label">{translator.t("LABEL.SKIP_DIRECT_SOURCE")}</span>
|
||||
<TagList tags={(*tags_skip_direct_source).clone()} />
|
||||
</div>
|
||||
<div class="tp__xtream_output__section">
|
||||
<span class="tp__xtream_output__label">{translator.t("LABEL.RESOLVE")}</span>
|
||||
<span class="tp__xtream_output__label">
|
||||
<TagList tags={(*tags_resolve).clone()} />
|
||||
</span>
|
||||
<div class="tp__target_output__output__section">
|
||||
<span class="tp__target_output__output__label">{translator.t("LABEL.RESOLVE")}</span>
|
||||
<TagList tags={(*tags_resolve).clone()} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,40 +1,66 @@
|
||||
use crate::app::components::popup_menu::PopupMenu;
|
||||
use crate::app::components::reveal_content::RevealContent;
|
||||
use crate::app::components::{AppIcon, Table, TableDefinition, TargetOutput, ToggleSwitch};
|
||||
use crate::hooks::use_service_context;
|
||||
use shared::model::ConfigTargetDto;
|
||||
use std::future;
|
||||
use std::rc::Rc;
|
||||
use log::info;
|
||||
use yew::prelude::*;
|
||||
use yew::suspense::use_future;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::model::{ConfigTargetDto};
|
||||
use crate::app::components::{PlaylistContext, Table, TableDefinition, TargetOutput, ToggleSwitch};
|
||||
use crate::app::components::reveal_content::RevealContent;
|
||||
use crate::hooks::use_service_context;
|
||||
use crate::app::components::menu_item::MenuItem;
|
||||
|
||||
const HEADERS: [&str; 11] = [
|
||||
"TABLE.EMPTY",
|
||||
"TABLE.ENABLED",
|
||||
"TABLE.NAME",
|
||||
"TABLE.OPTIONS",
|
||||
"TABLE.SORT",
|
||||
"TABLE.FILTER",
|
||||
"TABLE.OUTPUT",
|
||||
"TABLE.RENAME",
|
||||
"TABLE.MAPPING",
|
||||
"TABLE.PROCESSING_ORDER",
|
||||
"TABLE.WATCH",
|
||||
"TABLE.EMPTY",
|
||||
"TABLE.ENABLED",
|
||||
"TABLE.NAME",
|
||||
"TABLE.OPTIONS",
|
||||
"TABLE.SORT",
|
||||
"TABLE.FILTER",
|
||||
"TABLE.OUTPUT",
|
||||
"TABLE.RENAME",
|
||||
"TABLE.MAPPING",
|
||||
"TABLE.PROCESSING_ORDER",
|
||||
"TABLE.WATCH",
|
||||
];
|
||||
|
||||
#[function_component]
|
||||
pub fn TargetTable() -> Html {
|
||||
let translate = use_translation();
|
||||
let services = use_service_context();
|
||||
let playlist_ctx = use_context::<PlaylistContext>();
|
||||
let popup_anchor_ref = use_state(|| None::<web_sys::Element>);
|
||||
let popup_is_open = use_state(|| false);
|
||||
let selected_dto = use_state(|| None::<Rc<ConfigTargetDto>>);
|
||||
let table_definition = use_state(|| None::<Rc<TableDefinition<ConfigTargetDto>>>);
|
||||
|
||||
let handle_popup_close = {
|
||||
let set_is_open = popup_is_open.clone();
|
||||
Callback::from(move |()| {
|
||||
set_is_open.set(false);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_popup_onclick = {
|
||||
let set_selected_dto = selected_dto.clone();
|
||||
let set_anchor_ref = popup_anchor_ref.clone();
|
||||
let set_is_open = popup_is_open.clone();
|
||||
Callback::from(move |(dto, event): (Rc<ConfigTargetDto>, MouseEvent)| {
|
||||
if let Some(target) = event.target_dyn_into::<web_sys::Element>() {
|
||||
set_selected_dto.set(Some(dto.clone()));
|
||||
set_anchor_ref.set(Some(target));
|
||||
set_is_open.set(true);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let render_header_cell = {
|
||||
let translator = translate.clone();
|
||||
Callback::<usize, Html>::from(move |col| {
|
||||
html! {
|
||||
{
|
||||
if col < HEADERS.len() {
|
||||
translate.t(HEADERS[col])
|
||||
translator.t(HEADERS[col])
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
@@ -44,10 +70,20 @@ pub fn TargetTable() -> Html {
|
||||
};
|
||||
|
||||
let render_data_cell = {
|
||||
let popup_onclick = handle_popup_onclick.clone();
|
||||
Callback::<(usize, usize, Rc<ConfigTargetDto>), Html>::from(
|
||||
move |(_row, col, dto): (usize, usize, Rc<ConfigTargetDto>)| {
|
||||
move |(row, col, dto): (usize, usize, Rc<ConfigTargetDto>)| {
|
||||
match col {
|
||||
0 => html! { String::new() },
|
||||
0 => {
|
||||
let popup_onclick = popup_onclick.clone();
|
||||
html! {
|
||||
<button class="tp__icon-button"
|
||||
onclick={Callback::from(move |event: MouseEvent| popup_onclick.emit((dto.clone(), event)))}
|
||||
data-row={row.to_string()}>
|
||||
<AppIcon name="Popup"></AppIcon>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
1 => html! { <ToggleSwitch readonly={true} value={&dto.enabled} /> },
|
||||
2 => html! { &dto.name.to_string() },
|
||||
3 => html! { <RevealContent>{"Hello"}</RevealContent> },
|
||||
@@ -60,24 +96,13 @@ pub fn TargetTable() -> Html {
|
||||
10 => html! { dto.watch.as_ref().map_or_else(String::new, |w| format!("{w:?}"))},
|
||||
_ => html! {""},
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let table_definition = {
|
||||
let render_header_cell_cb = render_header_cell.clone();
|
||||
let render_data_cell_cb = render_data_cell.clone();
|
||||
use_state(|| Rc::new(TableDefinition::<ConfigTargetDto> {
|
||||
items: Rc::new(Vec::new()),
|
||||
num_cols: 11,
|
||||
render_header_cell: render_header_cell_cb,
|
||||
render_data_cell: render_data_cell_cb,
|
||||
}))
|
||||
})
|
||||
};
|
||||
|
||||
{
|
||||
// first register for config update
|
||||
let services_ctx = services.clone();
|
||||
let table_definition_state = table_definition.clone();
|
||||
let table_definition_state = table_definition.clone();
|
||||
let render_header_cell_cb = render_header_cell.clone();
|
||||
let render_data_cell_cb = render_data_cell.clone();
|
||||
let _ = use_future(|| async move {
|
||||
@@ -87,18 +112,17 @@ pub fn TargetTable() -> Html {
|
||||
let render_data_cell_cb = render_data_cell_cb.clone();
|
||||
if let Some(app_cfg) = cfg.clone() {
|
||||
let mut targets = vec![];
|
||||
info!("{:?}", &app_cfg.sources.sources);
|
||||
for source in &app_cfg.sources.sources {
|
||||
for target in &source.targets {
|
||||
targets.push(Rc::new(target.clone()));
|
||||
}
|
||||
}
|
||||
table_definition_state.set(Rc::new(TableDefinition::<ConfigTargetDto> {
|
||||
table_definition_state.set(Some(Rc::new(TableDefinition::<ConfigTargetDto> {
|
||||
items: Rc::new(targets),
|
||||
num_cols: 11,
|
||||
render_header_cell: render_header_cell_cb,
|
||||
render_data_cell: render_data_cell_cb,
|
||||
}));
|
||||
})));
|
||||
};
|
||||
future::ready(())
|
||||
}
|
||||
@@ -113,9 +137,33 @@ pub fn TargetTable() -> Html {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let handle_menu_edit = {
|
||||
let popup_is_open_state = popup_is_open.clone();
|
||||
Callback::from(move |name:String| {
|
||||
info!("Menu selected {name}");
|
||||
popup_is_open_state.set(false);
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__target-table">
|
||||
<Table::<ConfigTargetDto> definition={(*table_definition).clone()} />
|
||||
{
|
||||
if table_definition.is_some() {
|
||||
html! {
|
||||
<>
|
||||
<Table::<ConfigTargetDto> definition={(*table_definition).as_ref().unwrap().clone()} />
|
||||
<PopupMenu is_open={*popup_is_open} anchor_ref={(*popup_anchor_ref).clone()} on_close={handle_popup_close}>
|
||||
<MenuItem icon="Edit" name="edit" label={translate.t("LABEL.EDIT")} onclick={&handle_menu_edit}></MenuItem>
|
||||
<MenuItem style="tp__delete_action" icon="Delete" name="delete" label={translate.t("LABEL.DELETE")} onclick={&handle_menu_edit}></MenuItem>
|
||||
</PopupMenu>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
use wasm_bindgen::prelude::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{window, HtmlElement, MouseEvent};
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct PopupMenuProps {
|
||||
pub is_open: bool,
|
||||
pub anchor_ref: Option<web_sys::Element>,
|
||||
#[prop_or_default]
|
||||
pub on_close: Callback<()>,
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn PopupMenu(props: &PopupMenuProps) -> Html {
|
||||
let popup_ref = use_node_ref();
|
||||
|
||||
// Calculate popup position relative to anchor and keep inside viewport
|
||||
let style = {
|
||||
let is_open = props.is_open;
|
||||
let anchor_ref = props.anchor_ref.clone();
|
||||
use_memo((is_open, anchor_ref.clone()), move |(is_open, anchor_ref)| {
|
||||
if !*is_open || anchor_ref.is_none() {
|
||||
return "display: none;".to_string();
|
||||
}
|
||||
let anchor_ref = anchor_ref.as_ref().unwrap().clone();
|
||||
|
||||
let rect = anchor_ref.get_bounding_client_rect();
|
||||
let window = window().expect("no global window");
|
||||
let inner_width = window.inner_width().unwrap().as_f64().unwrap();
|
||||
let inner_height = window.inner_height().unwrap().as_f64().unwrap();
|
||||
|
||||
// Basic positioning below the anchor element
|
||||
let mut top = rect.bottom();
|
||||
let mut left = rect.left();
|
||||
|
||||
// Clamp popup within viewport width (assuming popup width ~200px)
|
||||
if left + 200.0 > inner_width {
|
||||
left = inner_width - 200.0;
|
||||
}
|
||||
if top + 150.0 > inner_height {
|
||||
// show above if no space below (assuming popup height ~150px)
|
||||
top = rect.top() - 150.0;
|
||||
}
|
||||
|
||||
format!(
|
||||
"position: fixed; top: {}px; left: {}px;",
|
||||
top, left
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
// Close popup when clicking outside of it
|
||||
{
|
||||
let popup_ref = popup_ref.clone();
|
||||
let on_close = props.on_close.clone();
|
||||
use_effect_with(props.is_open, move |is_open| {
|
||||
let handler = if *is_open {
|
||||
let handler = Closure::wrap(Box::new(move |event: MouseEvent| {
|
||||
if let Some(popup) = popup_ref.cast::<HtmlElement>() {
|
||||
if let Some(target) = event.target().and_then(|t| t.dyn_into::<HtmlElement>().ok()) {
|
||||
if !popup.contains(Some(&target)) {
|
||||
on_close.emit(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
|
||||
window()
|
||||
.unwrap()
|
||||
.add_event_listener_with_callback("mousedown", handler.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
Some(handler)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Cleanup-Funktion
|
||||
move || {
|
||||
if let Some(handler) = handler {
|
||||
window()
|
||||
.unwrap()
|
||||
.remove_event_listener_with_callback("mousedown", handler.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
html! {
|
||||
<div class="tp__popup-menu" ref={popup_ref} style={(*style).clone()}>
|
||||
<ul>
|
||||
{ for props.children.iter().map(|child| html! { <li>{child.clone()}</li> }) }
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -46,12 +46,12 @@ pub fn tag_list(props: &TagListProps) -> Html {
|
||||
};
|
||||
|
||||
let on_keypress = {
|
||||
let new_tag = new_tag.clone();
|
||||
let tag_state = tag_state.clone();
|
||||
let on_change = on_change.clone();
|
||||
// TODO
|
||||
// let new_tag = new_tag.clone();
|
||||
// let tag_state = tag_state.clone();
|
||||
// let on_change = on_change.clone();
|
||||
Callback::from(move |e: KeyboardEvent| {
|
||||
if e.key() == "Enter" {
|
||||
// TODO
|
||||
// let val = (*new_tag).trim().to_string();
|
||||
// if !val.is_empty() && !tag_state.contains(&val) {
|
||||
// let mut updated = (*tag_state).clone();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use log::info;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, Clone, PartialEq, Debug)]
|
||||
|
||||
Reference in New Issue
Block a user