undo: header buttons, shortcuts, wiring
- App provides HistoryHandle as context (both build modes; stacks stay empty on the server) and passes it to persist::init_hydrated - persist: init_hydrated starts undo tracking after the localStorage load (first captured state = saved state, not mock); clear_all and apply_import call undo::before_replace so they are undoable - header: Отменить/Вернуть buttons with disabled states driven by the reactive stack depths; Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y via a window keydown listener that skips editable targets (browser owns those) - SCSS: disabled control buttons dim out
This commit is contained in:
parent
99ccb0cdc7
commit
1f3184baf6
@ -32,11 +32,12 @@ pub fn App() -> impl IntoView {
|
|||||||
let store = Store::new(Dashboard::mock());
|
let store = Store::new(Dashboard::mock());
|
||||||
provide_context(store.clone());
|
provide_context(store.clone());
|
||||||
provide_context(ModalState::new());
|
provide_context(ModalState::new());
|
||||||
|
provide_context(crate::state::undo::HistoryHandle::new(store.clone()));
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
// Client-only: load the saved state after mount, then autosave.
|
// Client-only: load the saved state after mount, then autosave + undo.
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
crate::state::persist::init_hydrated(&store);
|
crate::state::persist::init_hydrated(&store, &expect_context::<crate::state::undo::HistoryHandle>());
|
||||||
|
|
||||||
let modal = expect_context::<ModalState>();
|
let modal = expect_context::<ModalState>();
|
||||||
|
|
||||||
|
|||||||
@ -6,18 +6,24 @@ use wasm_bindgen::JsCast;
|
|||||||
use crate::components::modal::ModalState;
|
use crate::components::modal::ModalState;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::state::persist::{ apply_import, clear_all, export_download };
|
use crate::state::persist::{ apply_import, clear_all, export_download };
|
||||||
|
use crate::state::undo::HistoryHandle;
|
||||||
|
|
||||||
/// Campaign header: editable name, campaign image, in-game date and
|
/// Campaign header: editable name, campaign image, in-game date and
|
||||||
/// session steppers, and the persistence controls.
|
/// session steppers, the persistence controls, and undo/redo.
|
||||||
///
|
///
|
||||||
/// Save downloads the dashboard as `dashboard.json`, Load imports one via
|
/// Save downloads the dashboard as `dashboard.json`, Load imports one via
|
||||||
/// a hidden file picker (with a confirm), Clear wipes to the blank
|
/// a hidden file picker (with a confirm), Clear wipes to the blank
|
||||||
/// campaign (with a confirm). Changes also autosave to localStorage
|
/// campaign (with a confirm). Changes also autosave to localStorage
|
||||||
/// (see `crate::state::persist`).
|
/// (see `crate::state::persist`) and feed the undo history (see
|
||||||
|
/// `crate::state::undo`); the undo/redo buttons and the Ctrl+Z /
|
||||||
|
/// Ctrl+Shift+Z / Ctrl+Y shortcuts revert the last edit bursts.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Header () -> impl IntoView {
|
pub fn Header () -> impl IntoView {
|
||||||
let state = expect_context::<Context>();
|
let state = expect_context::<Context>();
|
||||||
let modal = expect_context::<ModalState>();
|
let modal = expect_context::<ModalState>();
|
||||||
|
let history = expect_context::<HistoryHandle>();
|
||||||
|
let undo_depth = history.undo_depth();
|
||||||
|
let redo_depth = history.redo_depth();
|
||||||
let file_input = NodeRef::<leptos::html::Input>::new();
|
let file_input = NodeRef::<leptos::html::Input>::new();
|
||||||
let campaign = state.campaign();
|
let campaign = state.campaign();
|
||||||
let date = state.date();
|
let date = state.date();
|
||||||
@ -123,6 +129,16 @@ pub fn Header () -> impl IntoView {
|
|||||||
<button on:click=on_save>"Сохранить"</button>
|
<button on:click=on_save>"Сохранить"</button>
|
||||||
<button on:click=on_load>"Загрузить"</button>
|
<button on:click=on_load>"Загрузить"</button>
|
||||||
<button on:click=on_clear>"Очистить"</button>
|
<button on:click=on_clear>"Очистить"</button>
|
||||||
|
<button
|
||||||
|
on:click=move |_| history.undo()
|
||||||
|
disabled=move || undo_depth.get() == 0
|
||||||
|
title="Отменить (Ctrl+Z)"
|
||||||
|
>"Отменить"</button>
|
||||||
|
<button
|
||||||
|
on:click=move |_| history.redo()
|
||||||
|
disabled=move || redo_depth.get() == 0
|
||||||
|
title="Вернуть (Ctrl+Shift+Z или Ctrl+Y)"
|
||||||
|
>"Вернуть"</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<input type="file" accept=".json,application/json" style="display: none" node_ref=file_input on:change=on_file/>
|
<input type="file" accept=".json,application/json" style="display: none" node_ref=file_input on:change=on_file/>
|
||||||
|
|||||||
@ -59,6 +59,11 @@ header {
|
|||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
margin: 2px 5px;
|
margin: 2px 5px;
|
||||||
@include mixins.button;
|
@include mixins.button;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user