improve keystrokes, cleanup

This commit is contained in:
marlon-erler
2026-04-15 12:47:10 +00:00
parent ed8f523a89
commit 091c666ccc
31 changed files with 324 additions and 245 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ import { FilteredList } from "../Components/filteredList";
import { TaskCategoryBulkChangeViewModel } from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import { TaskViewModelToEntry } from "../Components/taskEntry";
import { allowDrop } from "../utility";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { ViewController } from "../viewController";
export function BoardKanbanPage(
coreViewModel: CoreViewModel,
@@ -75,7 +75,7 @@ function Column(
const view = (
<div
class="flex-column flex-no"
on:dragover={allowDrop}
on:dragover={ViewController.allowDrop}
on:drop={drop}
>
<div class="flex-row width-input">
+2 -3
View File
@@ -11,7 +11,6 @@ import { BoardViewToggleButton } from "../Components/boardViewToggleButton";
import { SearchModal } from "../Modals/searchModal";
import { TaskSettingsModal } from "../Modals/taskSettingsModal";
import { TaskViewModelToEntry } from "../Components/taskEntry";
import { setFocusWithDelay } from "../utility";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { ViewController } from "../viewController";
@@ -23,7 +22,7 @@ export function BoardPage(
// main content
const persistenceId = boardViewModel.boardInfo.fileId;
const pages = ViewController.boardPages.setItems(persistenceId, () =>
const pages = ViewController.boardPages.setState(persistenceId, () =>
React.createProxyState([boardViewModel.selectedPage], () => {
switch (boardViewModel.selectedPage.value) {
case BoardPageTypes.Kanban: {
@@ -54,7 +53,7 @@ export function BoardPage(
if (boardViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
setFocusWithDelay();
ViewController.setFocusWithDelay();
return TaskSettingsModal(
coreViewModel,
+2 -2
View File
@@ -15,8 +15,8 @@ import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { FilteredList } from "../Components/filteredList";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import { TaskViewModelToEntry } from "../Components/taskEntry";
import { allowDrop } from "../utility";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { ViewController } from "../viewController";
export function BoardStatusGridPage(
coreViewModel: CoreViewModel,
@@ -246,7 +246,7 @@ function StatusColumn(
const view = (
<div
class="status-column gap"
on:dragover={allowDrop}
on:dragover={ViewController.allowDrop}
on:drop={drop}
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
+2 -3
View File
@@ -2,14 +2,13 @@ import "./calendarPage.css";
import * as React from "bloatless-react";
import CalendarModel from "../../Model/Files/calendarModel";
import CalendarPageViewModel from "../../ViewModel/Pages/calendarPageViewModel";
import { MonthGrid } from "../Components/monthGrid";
import { TaskSettingsModal } from "../Modals/taskSettingsModal";
import { TaskViewModelToEntry } from "../Components/taskEntry";
import { setFocusWithDelay } from "../utility";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { PlaceholderView } from "../Components/placeholderView";
import { ViewController } from "../viewController";
export function CalendarPage(
coreViewModel: CoreViewModel,
@@ -95,7 +94,7 @@ export function CalendarPage(
) {
return <div></div>;
} else {
setFocusWithDelay();
ViewController.setFocusWithDelay();
return TaskSettingsModal(
coreViewModel,
+18 -12
View File
@@ -9,11 +9,19 @@
#chat-page #main #message-page .content #message-container {
--message-container-padding: var(--control-height);
padding-bottom: calc(var(--message-container-padding) + var(--content-padding)) !important;
padding-bottom: calc(
var(--message-container-padding) + var(--content-padding)
) !important;
}
#chat-page #main #message-page .content:has(.reply-preview-wrapper .reply-preview) #message-container {
--message-container-padding: calc(2 * var(--control-height) + var(--gap-between-tiles));
#chat-page
#main
#message-page
.content:has(.reply-preview-wrapper .reply-preview)
#message-container {
--message-container-padding: calc(
2 * var(--control-height) + var(--gap-between-tiles)
);
}
#message-page #message-container {
@@ -62,7 +70,7 @@
gap: var(--gap-between-tiles);
}
#message-page #composer .reply-preview-wrapper .reply-preview>div {
#message-page #composer .reply-preview-wrapper .reply-preview > div {
display: flex;
align-items: center;
gap: var(--text-gap);
@@ -81,7 +89,6 @@
}
@media (min-width: 700px) {
#message-page #composer .content-width-constraint,
#message-page #composer .input-width-constraint {
display: flex;
@@ -227,7 +234,7 @@
text-align: right;
}
.message-bubble>.inline-reply {
.message-bubble > .inline-reply {
cursor: pointer;
display: flex;
@@ -246,7 +253,7 @@
border-left: var(--outline-width) solid var(--primary);
}
.message-bubble>.message-reaction-row {
.message-bubble > .message-reaction-row {
max-width: var(--max-input-width);
width: 100%;
@@ -257,11 +264,11 @@
overflow: hidden;
}
.message-bubble>.message-reaction-row:has(button:not([count="0"])) {
.message-bubble > .message-reaction-row:has(button:not([count="0"])) {
display: grid !important;
}
.message-bubble>.message-reaction-row button {
.message-bubble > .message-reaction-row button {
min-height: unset;
height: 100%;
width: 100%;
@@ -271,7 +278,7 @@
animation: slide-up var(--animation-duration);
}
.message-bubble>.message-reaction-row button[count="0"] {
.message-bubble > .message-reaction-row button[count="0"] {
display: none;
}
@@ -285,7 +292,6 @@
}
}
.grid.message-reaction-filter {
grid-template-columns: repeat(3, 1fr);
}
}
+4 -6
View File
@@ -7,7 +7,6 @@ import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { ChatMessage } from "../Components/chatMessage";
import { MessageFilterModal } from "../Modals/messageFilterModal";
import { setFocusWithDelay } from "../utility";
import { ViewController } from "../viewController";
import { ReplyPreview } from "../Components/replyPreview";
@@ -34,7 +33,7 @@ export function MessagePage(
);
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
const replyPreview = ViewController.inlineReplies.setItems(
const replyPreview = ViewController.inlineReplies.setState(
persistenceId,
() =>
React.createProxyState(
@@ -70,10 +69,9 @@ export function MessagePage(
);
setTimeout(() => scrollDown(true), 100);
messagePageViewModel.replyingMessage.subscribeSilent(() => {
setFocusWithDelay();
})
setFocusWithDelay();
messagePageViewModel.focusSetter.subscribeSilent(() => {
ViewController.setFocusWithDelay();
});
return (
<div id="message-page">
+1 -1
View File
@@ -22,7 +22,7 @@ export function TaskPage(
);
const persistenceId = taskPageViewModel.chatViewModel.chatModel.id;
const pages = ViewController.taskPages.setItems(persistenceId, () =>
const pages = ViewController.taskPages.setState(persistenceId, () =>
React.createProxyState([taskPageViewModel.selectedBoardId], () => {
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
if (selectedBoardId == undefined) {
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { allowDrop } from "../utility";
import { ViewController } from "../viewController";
export function BoardEntry(boardViewModel: BoardViewModel) {
const view = (
@@ -10,7 +10,7 @@ export function BoardEntry(boardViewModel: BoardViewModel) {
class="tile colored-tile"
toggle:selected={boardViewModel.isSelected}
on:click={boardViewModel.select}
on:dragover={allowDrop}
on:dragover={ViewController.allowDrop}
on:drop={boardViewModel.handleDropBetweenBoards}
>
<span
+3 -4
View File
@@ -12,8 +12,7 @@ export function ChatMessage(
coreViewModel: CoreViewModel,
chatMessageViewModel: ChatMessageViewModel,
) {
const persistenceId = chatMessageViewModel.chatMessage.id;
const statusIcon = ViewController.messageIcons.setItems(persistenceId, () => React.createProxyState(
const statusIcon = React.createProxyState(
[chatMessageViewModel.status],
() => {
switch (chatMessageViewModel.status.value) {
@@ -26,8 +25,8 @@ export function ChatMessage(
default:
return "warning";
}
}
));
},
);
return (
<div
+3 -5
View File
@@ -4,15 +4,13 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { ViewController } from "../viewController";
export function InlineReply (
chatMessageViewModel: ChatMessageViewModel,
) {
export function InlineReply(chatMessageViewModel: ChatMessageViewModel) {
const reply = chatMessageViewModel.inlineReply;
if (reply == undefined) return <div></div>
if (reply == undefined) return <div></div>;
const scroll = () => {
ViewController.scrollToView(reply.chatMessage.id);
}
};
return (
<div class="inline-reply" on:click={scroll}>
+2 -2
View File
@@ -3,9 +3,9 @@ import "./monthGrid.css";
import * as React from "bloatless-react";
import { MonthGrid } from "../../Model/Files/calendarModel";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
import { allowDrop } from "../utility";
import { localeCompare } from "../../Model/Utility/utility";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { ViewController } from "../viewController";
export function MonthGrid<T>(
coreViewModel: CoreViewModel,
@@ -93,7 +93,7 @@ export function MonthGrid<T>(
on:click={select}
toggle:selected={isSelected}
toggle:today={isToday}
on:dragover={allowDrop}
on:dragover={ViewController.allowDrop}
on:drop={drop}
>
<div>
+8 -2
View File
@@ -11,12 +11,18 @@ export function ReplyPreview(
<div class="reply-preview">
<div class="surface blur">
<span class="secondary">{chatMessageViewModel.sender}</span>
<b class="ellipsis" subscribe:innerText={chatMessageViewModel.body}></b>
<b
class="ellipsis"
subscribe:innerText={chatMessageViewModel.body}
></b>
</div>
<button
class="standard square blur"
on:click={chatMessageViewModel.cancelReply}
aria-label={coreViewModel.translations.chatPage.message.cancelReplyAudioLabel}
aria-label={
coreViewModel.translations.chatPage.message
.cancelReplyAudioLabel
}
>
<span class="icon">close</span>
</button>
+4 -2
View File
@@ -4,13 +4,15 @@ import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { ColorPicker } from "../Components/colorPicker";
import { DangerousActionButton } from "../Components/dangerousActionButton";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { setFocusWithDelay } from "../utility";
import { ViewController } from "../viewController";
export function BoardSettingsModal(
coreViewModel: CoreViewModel,
boardViewModel: BoardViewModel,
) {
boardViewModel.isPresentingSettingsModal.subscribe(setFocusWithDelay);
boardViewModel.isPresentingSettingsModal.subscribe(
ViewController.setFocusWithDelay,
);
return (
<div
+2 -2
View File
@@ -8,8 +8,8 @@ import FileTransferViewModel, {
import ConnectionViewModel from "../../ViewModel/Global/connectionViewModel";
import StorageModel from "../../Model/Global/storageModel";
import { StringToTextSpan } from "../Components/textSpan";
import { reload } from "../utility";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { ViewController } from "../viewController";
export function DataTransferModalWrapper(
coreViewModel: CoreViewModel,
@@ -545,7 +545,7 @@ function DataReceptionModal(
]}
></div>
</main>
<button on:click={reload}>
<button on:click={ViewController.reload}>
{coreViewModel.translations.general.reloadAppButton}
<span class="icon">refresh</span>
</button>
+9 -2
View File
@@ -4,7 +4,7 @@ import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import { MessageReactionFilterButton } from "../Components/messageReactionFilterButton";
import { ReactionSymbols } from "../../Model/Chat/chatModel";
import { setFocusWithDelay } from "../utility";
import { ViewController } from "../viewController";
export function MessageFilterModal<T>(
coreViewModel: CoreViewModel,
@@ -16,7 +16,14 @@ export function MessageFilterModal<T>(
() => messagePageViewModel.reactionFilter.value == undefined,
);
messagePageViewModel.isFilterModalOpen.subscribe(setFocusWithDelay);
messagePageViewModel.isFilterModalOpen.subscribe((isOpen) => {
if (isOpen == false) return;
ViewController.setFocusWithDelay();
});
React.createProxyState([messagePageViewModel.isFilterModalOpen], () => {
if (messagePageViewModel.isFilterModalOpen.value == false) return;
ViewController.setFocusWithDelay();
});
return (
<div class="modal" toggle:open={messagePageViewModel.isFilterModalOpen}>
+5 -2
View File
@@ -4,7 +4,7 @@ import SearchViewModel from "../../ViewModel/Utility/searchViewModel";
import { StringToOption } from "../Components/option";
import { v4 } from "uuid";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { setFocusWithDelay } from "../utility";
import { ViewController } from "../viewController";
export function SearchModal<T>(
coreViewModel: CoreViewModel,
@@ -19,7 +19,10 @@ export function SearchModal<T>(
const suggestionId = v4();
isOpen.subscribe(setFocusWithDelay);
isOpen.subscribe((isOpen) => {
if (!isOpen) return;
ViewController.setFocusWithDelay();
});
return (
<div class="modal" toggle:open={isOpen} extended>
+2 -2
View File
@@ -16,7 +16,7 @@ export function ChatPage(
coreViewModel: CoreViewModel,
chatViewModel: ChatViewModel,
) {
ViewController.chatPages.items = React.createProxyState(
ViewController.chatPages.state = React.createProxyState(
[chatViewModel.selectedPage],
() => {
switch (chatViewModel.selectedPage.value) {
@@ -132,7 +132,7 @@ export function ChatPage(
</div>
<div
id="main"
children:set={ViewController.chatPages.items}
children:set={ViewController.chatPages.state}
></div>
</div>
</article>
-23
View File
@@ -1,23 +0,0 @@
export function allowDrop(event: DragEvent) {
event.preventDefault();
}
export function allowDrag(event: DragEvent) {
event.dataTransfer?.setData("text", "");
}
export function reload() {
window.location.reload();
}
export function setFocus() {
const focusedInModal = document.querySelector(
".modal[open] #focused",
) as HTMLElement;
if (focusedInModal) return focusedInModal.focus();
document.getElementById("focused")?.focus();
}
export function setFocusWithDelay() {
setTimeout(setFocus, 100);
}
+46 -22
View File
@@ -1,48 +1,72 @@
import * as React from "bloatless-react";
class ItemTracker<T> {
items: React.State<T | undefined> | undefined;
class Tracker<T> {
state: React.State<T> | undefined;
}
class ViewTrackerMap<T> {
trackers = new Map<string, ItemTracker<T>>();
class TrackerMap<T> {
trackers = new Map<string, Tracker<T>>();
private register = (key: string): ItemTracker<T> => {
private register = (key: string): Tracker<T> => {
if (this.trackers.has(key)) {
return this.trackers.get(key);
}
const tracker = new ItemTracker<T>();
const tracker = new Tracker<T>();
this.trackers.set(key, tracker);
return tracker;
};
setItems = (
setState = (
key: string,
views: () => React.State<T>,
stateBuilder: () => React.State<T>,
): React.State<T | undefined> | undefined => {
const tracker: ItemTracker<T> = this.register(key);
const tracker: Tracker<T> = this.register(key);
if (tracker.items != undefined) {
return tracker.items;
if (tracker.state != undefined) {
return tracker.state;
} else {
tracker.items = views();
return tracker.items;
tracker.state = stateBuilder();
return tracker.state;
}
};
}
export class ViewController {
static readonly chatPages = new ItemTracker<HTMLElement>();
static readonly taskPages = new ViewTrackerMap<HTMLElement>();
static readonly boardPages = new ViewTrackerMap<HTMLElement>();
static readonly inlineReplies = new ViewTrackerMap<HTMLElement>();
static readonly messageIcons = new ViewTrackerMap<string>();
static readonly chatPages = new Tracker<HTMLElement>();
static readonly taskPages = new TrackerMap<HTMLElement>();
static readonly boardPages = new TrackerMap<HTMLElement>();
static readonly inlineReplies = new TrackerMap<HTMLElement>();
static scrollToView(id: string) {
static readonly scrollToView = (id: string) => {
const element = document.getElementById(id);
if (!element) return
if (!element) return;
element.scrollIntoView();
element.setAttribute("scrolled", "");
setTimeout(()=>element.removeAttribute("scrolled"), 1000);
}
setTimeout(() => element.removeAttribute("scrolled"), 1000);
};
static readonly allowDrop = (event: DragEvent) => {
event.preventDefault();
};
static readonly allowDrag = (event: DragEvent) => {
event.dataTransfer?.setData("text", "");
};
static readonly reload = () => {
window.location.reload();
};
static readonly setFocus = () => {
const focusedInModal = document.querySelector(
".modal[open] #focused",
) as HTMLElement;
if (focusedInModal) return focusedInModal.focus();
document.getElementById("focused")?.focus();
};
static readonly setFocusWithDelay = () => {
console.trace("focus");
setTimeout(this.setFocus, 100);
};
}