Skip to content

Commit 9f4546d

Browse files
committed
feat: store modal's position to the local storage
1 parent f73c1f5 commit 9f4546d

File tree

7 files changed

+97
-25
lines changed

7 files changed

+97
-25
lines changed

_build/gpm.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: modAI
2-
version: 0.12.0-alpha4
2+
version: 0.12.0-alpha5
33
lowCaseName: modai
44
namespace: modAI
55
author: 'John Peca'

_build/js/src/ui/localChat/modalBuilder.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { createElement } from '../utils';
1+
import { createElement, debounce } from '../utils';
22
import { renderMessage } from './messageHandlers';
33
import { scrollToBottom } from './modalActions';
44
import { buildModalChat } from './modalChat';
55
import { buildModalHeader } from './modalHeader';
66
import { buildModalInput } from './modalInput';
7+
import { loadModalState, saveModalState } from './state';
78
import { chatHistory } from '../../chatHistory';
89
import { globalState } from '../../globalState';
910
import { lng } from '../../lng';
@@ -12,6 +13,8 @@ import { createModAIShadow } from '../dom/modAIShadow';
1213
import type { Modal, LocalChatConfig } from './types';
1314
import type { UpdatableHTMLElement } from '../../chatHistory';
1415

16+
const debouncedSaveModalState = debounce(saveModalState, 300);
17+
1518
export const buildModal = (config: LocalChatConfig) => {
1619
const { shadow, shadowRoot } = createModAIShadow<Modal>(true, () => {
1720
scrollToBottom('instant');
@@ -22,7 +25,17 @@ export const buildModal = (config: LocalChatConfig) => {
2225
ariaLabel: lng('modai.ui.modai_assistant_chat_dialog'),
2326
});
2427

28+
const modalState = loadModalState();
29+
if (modalState.position) {
30+
chatModal.style.width = modalState.position.width ?? '';
31+
chatModal.style.height = modalState.position.height ?? '';
32+
chatModal.style.top = modalState.position.top ?? '';
33+
chatModal.style.left = modalState.position.left ?? '';
34+
chatModal.style.transform = 'none';
35+
}
36+
2537
const resizeObserver = new ResizeObserver(() => {
38+
debouncedSaveModalState();
2639
const msg = globalState.modal.chatMessages.lastElementChild as UpdatableHTMLElement | null;
2740

2841
if (msg) {

_build/js/src/ui/localChat/modalHeader.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { globalState } from '../../globalState';
66
import { lng } from '../../lng';
77
import { icon } from '../dom/icon';
88
import { expand, minimize, x } from '../icons';
9+
import { saveModalState } from './state';
910

1011
const centerModal = (element: HTMLElement) => {
1112
const modalWidth = element.offsetWidth;
@@ -31,15 +32,20 @@ export const buildModalHeader = () => {
3132
{ ariaLabel: lng('modai.ui.close_dialog') },
3233
);
3334

35+
const isMaximized = globalState.modal.modal.style.width === '90%';
36+
3437
const buttonsWrapper = createElement('div', 'buttonsWrapper', [
3538
button(
36-
icon(24, expand),
39+
icon(24, isMaximized ? minimize : expand),
3740
(e) => {
3841
const self = e.currentTarget as HTMLButtonElement;
3942

4043
if (globalState.modal.modal.style.width === '90%') {
4144
globalState.modal.modal.style.width = '';
4245
globalState.modal.modal.style.height = '';
46+
globalState.modal.modal.style.transform = 'none';
47+
48+
saveModalState();
4349

4450
self.ariaLabel = lng('modai.ui.maximize_dialog');
4551
self.innerHTML = '';
@@ -51,6 +57,9 @@ export const buildModalHeader = () => {
5157

5258
globalState.modal.modal.style.width = '90%';
5359
globalState.modal.modal.style.height = '90%';
60+
globalState.modal.modal.style.transform = 'none';
61+
62+
saveModalState();
5463

5564
self.ariaLabel = lng('modai.ui.minimize_dialog');
5665
self.innerHTML = '';
@@ -60,10 +69,7 @@ export const buildModalHeader = () => {
6069
},
6170
'',
6271
{
63-
ariaLabel:
64-
globalState.modal.modal.style.width === '90%'
65-
? lng('modai.ui.minimize_dialog')
66-
: lng('modai.ui.maximize_dialog'),
72+
ariaLabel: isMaximized ? lng('modai.ui.minimize_dialog') : lng('modai.ui.maximize_dialog'),
6773
},
6874
),
6975
closeModalBtn,
@@ -81,6 +87,7 @@ export const buildModalHeader = () => {
8187
endDrag();
8288
document.removeEventListener('mousemove', drag);
8389
document.removeEventListener('mouseup', endDrag);
90+
saveModalState();
8491
});
8592
});
8693

_build/js/src/ui/localChat/state.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { globalState } from '../../globalState';
22

3+
import type { ModalState } from './types';
34
import type { Button } from '../dom/button';
45

56
export const setLoadingState = (loading: boolean) => {
@@ -53,3 +54,34 @@ export const setLoadingState = (loading: boolean) => {
5354
}
5455
});
5556
};
57+
58+
const MODAL_STORAGE_KEY = 'modai__state';
59+
60+
export const saveModalState = () => {
61+
const currentState = loadModalState();
62+
currentState.position = {
63+
width: globalState.modal.modal.style.width,
64+
height: globalState.modal.modal.style.height,
65+
left: globalState.modal.modal.style.left,
66+
top: globalState.modal.modal.style.top,
67+
};
68+
69+
try {
70+
localStorage.setItem(MODAL_STORAGE_KEY, JSON.stringify(currentState));
71+
} catch {
72+
/* not needed */
73+
}
74+
};
75+
76+
export const loadModalState = (): ModalState => {
77+
try {
78+
const savedStateString = localStorage.getItem(MODAL_STORAGE_KEY);
79+
if (savedStateString) {
80+
return JSON.parse(savedStateString);
81+
}
82+
83+
return {};
84+
} catch {
85+
return {};
86+
}
87+
};

_build/js/src/ui/localChat/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,12 @@ export type LocalChatConfig = {
6767
mediaSource?: number | string;
6868
};
6969
};
70+
71+
export type ModalState = {
72+
position?: {
73+
width?: string;
74+
height?: string;
75+
left?: string;
76+
top?: string;
77+
};
78+
};

_build/js/src/ui/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,14 @@ export const createElement = <K extends keyof HTMLElementTagNameMap>(
5050
export const nlToBr = (content: string) => {
5151
return /<[^>]*>/g.test(content) ? content : content.replace(/\r\n|\n|\r/g, '<br>');
5252
};
53+
54+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
55+
export function debounce<T extends (...args: any[]) => void>(func: T, delay: number) {
56+
let timeoutId: number | undefined;
57+
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
58+
clearTimeout(timeoutId);
59+
timeoutId = setTimeout(() => {
60+
func.apply(this, args);
61+
}, delay);
62+
};
63+
}

0 commit comments

Comments
 (0)