[[package]]
name = "bitflags"
-version = "2.6.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
dependencies = [
"serde",
]
[[package]]
name = "clap"
-version = "4.5.24"
+version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd"
+checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
dependencies = [
"clap_builder",
"clap_derive",
[[package]]
name = "clap_builder"
-version = "4.5.24"
+version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd"
+checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
dependencies = [
"anstream",
"anstyle",
"gloo",
"ron",
"serde",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"sqlx",
"strum",
"strum_macros",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
"tokio",
"tower",
"tower-http",
[[package]]
name = "rustls"
-version = "0.23.20"
+version = "0.23.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
+checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
dependencies = [
"log",
"once_cell",
"serde_json",
"sha2",
"smallvec",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
"tokio",
"tokio-stream",
"tracing",
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
"tracing",
"whoami",
]
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
"tracing",
"whoami",
]
[[package]]
name = "syn"
-version = "2.0.95"
+version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
+checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
[[package]]
name = "thiserror"
-version = "2.0.9"
+version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
- "thiserror-impl 2.0.9",
+ "thiserror-impl 2.0.11",
]
[[package]]
[[package]]
name = "thiserror-impl"
-version = "2.0.9"
+version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
[[package]]
name = "tokio"
-version = "1.42.0"
+version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
+checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
[[package]]
name = "tokio-macros"
-version = "2.4.0"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
.drag-handle {
width: 20px;
height: 20px;
+ display: inline-block;
+ vertical-align: bottom;
background-color: blue;
}
border: 0;
}
-
.recipe-item {
padding: 4px;
}
h1 {
text-align: center;
}
+ }
+
+ #recipe-edit {
+
+ .drag-handle {
+ cursor: move;
+ }
.group {
border: 0.1em solid lighten($color-3, 30%);
border: 0.1em solid lighten($color-3, 30%);
}
+ .dropzone-group,
+ .dropzone-step {
+ height: 10px;
+ background-color: white;
+
+ &.active {
+ background-color: blue;
+ }
+
+ &.hover {
+ background-color: red;
+ }
+ }
+
+
#hidden-templates {
display: none;
}
NULL
);
-INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
-VALUES (1, 1, 'Croissant au jambon', true);
+INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
+VALUES (1, 1, 'Croissant au jambon', true, '2025-01-07T10:41:05.697884837+00:00');
-INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
-VALUES (2, 1, 'Gratin de thon aux olives', true);
+INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
+VALUES (2, 1, 'Gratin de thon aux olives', true, '2025-01-07T10:41:05.697884837+00:00');
-INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
-VALUES (3, 1, 'Saumon en croute', true);
+INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
+VALUES (3, 1, 'Saumon en croute', true, '2025-01-07T10:41:05.697884837+00:00');
-INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
-VALUES (4, 2, 'Ouiche lorraine', true);
+INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
+VALUES (4, 2, 'Ouiche lorraine', true, '2025-01-07T10:41:05.697884837+00:00');
-- Groups, steps and ingredients for 'Gratin de thon'.
[is_admin] INTEGER NOT NULL DEFAULT FALSE
) STRICT;
+CREATE INDEX [validation_token_index] ON [User]([validation_token]);
CREATE UNIQUE INDEX [User_email_index] ON [User]([email]);
CREATE TABLE [UserLoginToken] (
[difficulty] INTEGER NOT NULL DEFAULT 0,
[servings] INTEGER DEFAULT 4,
[is_published] INTEGER NOT NULL DEFAULT FALSE,
+ [creation_datetime] TEXT NOT NULL,
FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL
) STRICT;
+use chrono::prelude::*;
+use itertools::Itertools;
+
use super::{Connection, DBError, Result};
use crate::data::model;
SELECT [id], [title]
FROM [Recipe]
WHERE [is_published] = true AND ([lang] = $1 OR [user_id] = $2)
- ORDER BY [title]
+ ORDER BY [title] COLLATE NOCASE
"#,
)
.bind(lang)
SELECT [id], [title]
FROM [Recipe]
WHERE [is_published] = true AND [lang] = $1
- ORDER BY [title]
+ ORDER BY [title] COLLATE NOCASE
"#,
)
.bind(lang)
.map_err(DBError::from)
}
+ pub async fn can_edit_recipe_all_groups(
+ &self,
+ user_id: i64,
+ group_ids: &[i64],
+ ) -> Result<bool> {
+ let params = (0..group_ids.len())
+ .map(|n| format!("${}", n + 2))
+ .join(", ");
+ let query_str = format!(
+ r#"
+SELECT COUNT(*)
+FROM [Recipe]
+INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
+WHERE [Group].[id] IN ({}) AND [user_id] = $1
+ "#,
+ params
+ );
+
+ let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
+ for id in group_ids {
+ query = query.bind(id);
+ }
+ Ok(query.fetch_one(&self.pool).await? == group_ids.len() as u64)
+ }
+
pub async fn can_edit_recipe_step(&self, user_id: i64, step_id: i64) -> Result<bool> {
sqlx::query_scalar(
r#"
.await?;
let db_result = sqlx::query(
- "INSERT INTO [Recipe] ([user_id], [lang], [title]) VALUES ($1, $2, '')",
+ "INSERT INTO [Recipe] ([user_id], [lang], [title], [creation_datetime]) VALUES ($1, $2, '', $3)",
)
.bind(user_id)
.bind(lang)
+ .bind(Utc::now())
.execute(&mut *tx)
.await?;
.map_err(DBError::from)
}
+ pub async fn set_groups_order(&self, group_ids: &[i64]) -> Result<()> {
+ let mut tx = self.tx().await?;
+
+ for (order, id) in group_ids.iter().enumerate() {
+ sqlx::query("UPDATE [Group] SET [order] = $2 WHERE [id] = $1")
+ .bind(id)
+ .bind(order as i64)
+ .execute(&mut *tx)
+ .await?;
+ }
+
+ tx.commit().await?;
+
+ Ok(())
+ }
+
pub async fn add_recipe_step(&self, group_id: i64) -> Result<i64> {
let db_result = sqlx::query("INSERT INTO [Step] ([group_id]) VALUES ($1)")
.bind(group_id)
Ok(GetTokenResetPasswordResult::Ok(token))
}
+ pub async fn is_reset_password_token_valid(
+ &self,
+ token: &str,
+ validation_time: Duration,
+ ) -> Result<bool> {
+ if let Some(Some(db_datetime)) = sqlx::query_scalar::<_, Option<DateTime<Utc>>>(
+ r#"
+SELECT [password_reset_datetime]
+FROM [User]
+WHERE [password_reset_token] = $1
+ "#,
+ )
+ .bind(token)
+ .fetch_optional(&self.pool)
+ .await?
+ {
+ Ok(Utc::now() - db_datetime <= validation_time)
+ } else {
+ Ok(false)
+ }
+ }
+
pub async fn reset_password(
&self,
new_password: &str,
"/recipe/set_group_comment",
put(services::ron::set_group_comment),
)
+ .route(
+ "/recipe/set_groups_order",
+ put(services::ron::set_group_orders),
+ )
.route("/recipe/add_step", post(services::ron::add_step))
.route("/recipe/remove_step", delete(services::ron::rm_step))
.route(
}
}
+async fn check_user_rights_recipe_groups(
+ connection: &db::Connection,
+ user: &Option<model::User>,
+ group_ids: &[i64],
+) -> Result<()> {
+ if user.is_none()
+ || !connection
+ .can_edit_recipe_all_groups(user.as_ref().unwrap().id, group_ids)
+ .await?
+ {
+ Err(ErrorResponse::from(ron_error(
+ StatusCode::UNAUTHORIZED,
+ NOT_AUTHORIZED_MESSAGE,
+ )))
+ } else {
+ Ok(())
+ }
+}
+
async fn check_user_rights_recipe_step(
connection: &db::Connection,
user: &Option<model::User>,
Ok(StatusCode::OK)
}
+#[debug_handler]
+pub async fn set_group_orders(
+ State(connection): State<db::Connection>,
+ Extension(user): Extension<Option<model::User>>,
+ ExtractRon(ron): ExtractRon<common::ron_api::SetGroupOrders>,
+) -> Result<impl IntoResponse> {
+ check_user_rights_recipe_groups(&connection, &user, &ron.group_ids).await?;
+ connection.set_groups_order(&ron.group_ids).await?;
+ Ok(StatusCode::OK)
+}
+
#[debug_handler]
pub async fn add_step(
State(connection): State<db::Connection>,
#[debug_handler]
pub async fn reset_password_get(
+ State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Query(query): Query<HashMap<String, String>>,
) -> Result<Response> {
if let Some(reset_token) = query.get("reset_token") {
- Ok(ResetPasswordTemplate {
- user,
- tr,
- reset_token,
- message: "",
- message_password: "",
+ // Check if the token is valid.
+ if connection
+ .is_reset_password_token_valid(
+ reset_token,
+ Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION),
+ )
+ .await?
+ {
+ Ok(ResetPasswordTemplate {
+ user,
+ tr,
+ reset_token,
+ message: "",
+ message_password: "",
+ }
+ .into_response())
+ } else {
+ Ok(
+ MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
+ .into_response(),
+ )
}
- .into_response())
} else {
Ok(
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
<input id="input-delete" type="button" value="{{ tr.t(Sentence::RecipeDelete) }}" />
<div id="groups-container">
-
+ <div class="dropzone-group"></div>
</div>
<input id="input-add-group" type="button" value="{{ tr.t(Sentence::RecipeAddAGroup) }}" />
<div id="hidden-templates">
<div class="group">
- <div class="drag-handle"></div>
+ <span class="drag-handle"></span>
<label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label>
<input class="input-group-name" type="text" />
<input class="input-group-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveGroup) }}" />
- <div class="steps"></div>
+ <div class="steps">
+ <div class="dropzone-step"></div>
+ </div>
<input class="input-add-step" type="button" value="{{ tr.t(Sentence::RecipeAddAStep) }}" />
</div>
<div class="step">
- <div class="drag-handle"></div>
+ <span class="drag-handle"></span>
<label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label>
<textarea class="text-area-step-action"></textarea>
pub comment: String,
}
+#[derive(Serialize, Deserialize, Clone)]
+pub struct SetGroupOrders {
+ pub group_ids: Vec<i64>,
+}
+
#[derive(Serialize, Deserialize, Clone)]
pub struct AddRecipeStep {
pub group_id: i64,
web-sys = { version = "0.3", features = [
"console",
"Document",
- "Element",
- "HtmlElement",
"Node",
+ "NodeList",
"Window",
"Location",
"EventTarget",
+ "DragEvent",
+ "DataTransfer",
+ "KeyboardEvent",
+ "Element",
+ "HtmlElement",
+ "HtmlDivElement",
"HtmlLabelElement",
"HtmlInputElement",
"HtmlTextAreaElement",
"HtmlSelectElement",
"HtmlDialogElement",
- "KeyboardEvent",
] }
gloo = "0.11"
use gloo::{
- events::EventListener,
+ console::log,
+ events::{EventListener, EventListenerOptions},
net::http::Request,
utils::{document, window},
};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
-use web_sys::{Element, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, KeyboardEvent};
+use web_sys::{
+ DragEvent, Element, HtmlDivElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
+ KeyboardEvent,
+};
use common::ron_api;
use crate::{
modal_dialog, request,
toast::{self, Level},
- utils::{by_id, selector, selector_and_clone, SelectorExt},
+ utils::{by_id, selector, selector_all, selector_and_clone, SelectorExt},
};
async fn reload_recipes_list(current_recipe_id: i64) {
})
.forget();
+ let group_dropzone: Element = selector(".dropzone-group");
+ setup_dragzone_events(&group_dropzone);
+
+ fn setup_dragzone_events(dropzone: &Element) {
+ EventListener::new_with_options(
+ dropzone,
+ "dragover",
+ EventListenerOptions::enable_prevent_default(),
+ |event| {
+ let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
+ let drag_data = event
+ .data_transfer()
+ .unwrap()
+ .get_data("text/plain")
+ .unwrap();
+
+ if drag_data.starts_with("group") {
+ event.prevent_default();
+ // event.data_transfer().unwrap().set_effect_allowed("move");
+ log!("drag over");
+ event
+ .target()
+ .unwrap()
+ .dyn_into::<Element>()
+ .unwrap()
+ .set_class_name("dropzone-group hover");
+ }
+ },
+ )
+ .forget();
+
+ EventListener::new(dropzone, "dragleave", |event| {
+ let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
+ let drag_data = event
+ .data_transfer()
+ .unwrap()
+ .get_data("text/plain")
+ .unwrap();
+
+ if drag_data.starts_with("group") {
+ log!("drag leave");
+ event
+ .target()
+ .unwrap()
+ .dyn_into::<Element>()
+ .unwrap()
+ .set_class_name("dropzone-group active");
+ }
+ })
+ .forget();
+
+ EventListener::new(dropzone, "drop", |event| {
+ let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
+ let drag_data = event
+ .data_transfer()
+ .unwrap()
+ .get_data("text/plain")
+ .unwrap();
+
+ if drag_data.starts_with("group") {
+ let id: i64 = drag_data[6..].parse().unwrap();
+ let target: Element = event.target().unwrap().dyn_into().unwrap();
+ let group: Element = by_id(&format!("group-{}", id));
+ let group_dropzone: Element = by_id(&format!("dropzone-group-{}", id));
+ target.after_with_node_1(&group).unwrap();
+ group.after_with_node_1(&group_dropzone).unwrap();
+
+ send_groups_order();
+ }
+ })
+ .forget();
+ }
+
+ fn send_groups_order() {
+ spawn_local(async move {
+ let group_ids = by_id::<Element>("groups-container")
+ .selector_all::<Element>(".group")
+ .into_iter()
+ .map(|e| e.get_attribute("id").unwrap()[6..].parse::<i64>().unwrap())
+ .collect();
+
+ let body = ron_api::SetGroupOrders { group_ids };
+ let _ = request::put::<(), _>("recipe/set_groups_order", body).await;
+ });
+ }
+
fn create_tag_elements<T>(recipe_id: i64, tags: &[T])
where
T: AsRef<str>,
let groups_container: Element = by_id("groups-container");
groups_container.append_child(&group_element).unwrap();
+ let dropzone_group: Element = selector_and_clone(".dropzone-group");
+ dropzone_group
+ .set_attribute("id", &format!("dropzone-group-{}", group.id))
+ .unwrap();
+ groups_container.append_child(&dropzone_group).unwrap();
+ setup_dragzone_events(&dropzone_group);
+
+ let drag_handle: Element = group_element.selector(".drag-handle");
+ EventListener::new(&drag_handle, "mousedown", |event| {
+ event
+ .target()
+ .unwrap()
+ .dyn_into::<Element>()
+ .unwrap()
+ .parent_element()
+ .unwrap()
+ .set_attribute("draggable", "true")
+ .unwrap();
+ })
+ .forget();
+
+ EventListener::new(&drag_handle, "mouseup", |event| {
+ event
+ .target()
+ .unwrap()
+ .dyn_into::<Element>()
+ .unwrap()
+ .parent_element()
+ .unwrap()
+ .set_attribute("draggable", "false")
+ .unwrap();
+ })
+ .forget();
+
+ EventListener::new(&group_element, "dragstart", |event| {
+ let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
+ let target_element: Element = event.target().unwrap().dyn_into().unwrap();
+ if target_element.get_attribute("class").unwrap() == "group" {
+ // Highlight where the group can be droppped.
+ for dp in selector_all::<HtmlDivElement>(".dropzone-group") {
+ dp.set_class_name("dropzone-group active");
+ }
+ event
+ .data_transfer()
+ .unwrap()
+ .set_data("text/plain", &target_element.get_attribute("id").unwrap())
+ .unwrap();
+ event.data_transfer().unwrap().set_effect_allowed("move");
+ }
+ })
+ .forget();
+
+ EventListener::new(&group_element, "dragend", |event| {
+ // let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
+ event
+ .target()
+ .unwrap()
+ .dyn_into::<Element>()
+ .unwrap()
+ .set_attribute("draggable", "false")
+ .unwrap();
+
+ let target_element: Element = event.target().unwrap().dyn_into().unwrap();
+ if target_element.get_attribute("class").unwrap() == "group" {
+ for dp in selector_all::<HtmlDivElement>(".dropzone-group") {
+ dp.set_class_name("dropzone-group");
+ }
+ }
+ })
+ .forget();
+
// Group name.
let name = group_element.selector::<HtmlInputElement>(".input-group-name");
name.set_value(&group.name);
let body = ron_api::RemoveRecipeGroup { group_id };
let _ = request::delete::<(), _>("recipe/remove_group", body).await;
by_id::<Element>(&format!("group-{}", group_id)).remove();
+ by_id::<Element>(&format!("dropzone-group-{}", group_id)).remove();
}
});
})
.unwrap();
group_element.append_child(&step_element).unwrap();
+ let dropzone_step: Element = selector_and_clone(".dropzone-step");
+ dropzone_step
+ .set_attribute("id", &format!("dropzone-step-{}", step.id))
+ .unwrap();
+ group_element.append_child(&dropzone_step).unwrap();
+
+ let drag_handle: Element = step_element.selector(".drag-handle");
+
+ EventListener::new(&drag_handle, "mousedown", |event| {
+ event
+ .target()
+ .unwrap()
+ .dyn_into::<Element>()
+ .unwrap()
+ .parent_element()
+ .unwrap()
+ .set_attribute("draggable", "true")
+ .unwrap();
+ })
+ .forget();
+
+ EventListener::new(&drag_handle, "mouseup", |event| {
+ event
+ .target()
+ .unwrap()
+ .dyn_into::<Element>()
+ .unwrap()
+ .parent_element()
+ .unwrap()
+ .set_attribute("draggable", "false")
+ .unwrap();
+ })
+ .forget();
+
+ EventListener::new(&step_element, "dragstart", |event| {
+ let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
+ // let target_element: Element = event.target().unwrap().dyn_into().unwrap();
+ // if target_element.get_attribute("class").unwrap() == "step" {
+ // Highlight where the step can be droppped.
+ log!("START DRAG STEP");
+ // log!(event);
+ // }
+ })
+ .forget();
+ EventListener::new(&step_element, "dragend", |event| {
+ let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
+ // let target_element: Element = event.target().unwrap().dyn_into().unwrap();
+ // if target_element.get_attribute("class").unwrap() == "step" {
+ // Highlight where the step can be droppped.
+ event
+ .target()
+ .unwrap()
+ .dyn_into::<Element>()
+ .unwrap()
+ .set_attribute("draggable", "false")
+ .unwrap();
+
+ log!("STOP DRAG STEP");
+ // log!(event);
+ // }
+ })
+ .forget();
+
// Step action.
let action: HtmlTextAreaElement = step_element.selector(".text-area-step-action");
action.set_value(&step.action);
let body = ron_api::RemoveRecipeStep { step_id };
let _ = request::delete::<(), _>("recipe/remove_step", body).await;
by_id::<Element>(&format!("step-{}", step_id)).remove();
+ by_id::<Element>(&format!("dropzone-step-{}", step_id)).remove();
}
});
})
-use gloo::utils::document;
+use gloo::{console::log, utils::document};
use wasm_bindgen::prelude::*;
use web_sys::Element;
fn selector<T>(&self, selectors: &str) -> T
where
T: JsCast;
+
+ fn selector_all<T>(&self, selectors: &str) -> Vec<T>
+ where
+ T: JsCast;
}
impl SelectorExt for Element {
.dyn_into::<T>()
.unwrap()
}
+
+ fn selector_all<T>(&self, selectors: &str) -> Vec<T>
+ where
+ T: JsCast,
+ {
+ self.query_selector_all(selectors)
+ .unwrap()
+ .values()
+ .into_iter()
+ .map(|e| e.unwrap().dyn_into::<T>().unwrap())
+ .collect()
+ }
}
pub fn selector<T>(selectors: &str) -> T
.unwrap()
}
+pub fn selector_all<T>(selectors: &str) -> Vec<T>
+where
+ T: JsCast,
+{
+ document()
+ .query_selector_all(selectors)
+ .unwrap()
+ .values()
+ .into_iter()
+ .map(|e| e.unwrap().dyn_into::<T>().unwrap())
+ .collect()
+}
+
pub fn selector_and_clone<T>(selectors: &str) -> T
where
T: JsCast,