mirror of
https://github.com/sechan100/daily-routine-2.git
synced 2026-07-22 12:00:31 +00:00
Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91878346eb | ||
|
|
4f47f2b64d | ||
|
|
7ba75a3e02 | ||
|
|
56481d2d92 | ||
|
|
2737e80003 | ||
|
|
38fb966643 | ||
|
|
39b6b78ae4 | ||
|
|
04a04b71bf | ||
|
|
e3b567538a | ||
|
|
0b033a44dd |
18 changed files with 927 additions and 1604 deletions
1
.env
1
.env
|
|
@ -1 +0,0 @@
|
|||
ICLOUD_DIRECTORY = /Users/sechan/Library/Mobile Documents/iCloud~md~obsidian/Documents/main/.obsidian/plugins/daily-routine-2/
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"id": "daily-routine",
|
||||
"name": "Daily Routine",
|
||||
"version": "1.0.2",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Manage your daily tasks as 'ROUTINES' and organize your everyday life more effectively.",
|
||||
"author": "sechan100",
|
||||
"id": "daily-routine",
|
||||
"name": "Daily Routine",
|
||||
"version": "1.0.8",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Manage your daily tasks as 'Routine' and organize your everyday life more effectively.",
|
||||
"author": "sechan100",
|
||||
"repo": "sechan100/daily-routine-2",
|
||||
"authorUrl": "https://www.linkedin.com/in/sechan-beak-80b3462b0",
|
||||
"authorUrl": "https://www.linkedin.com/in/sechan-baek-80b3462b0",
|
||||
"fundingUrl": "https://buymeacoffee.com/sechan100",
|
||||
"isDesktopOnly": false
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
2087
package-lock.json
generated
2087
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "daily-routine-obsidian-plugin",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"esbuild": "^0.27.2",
|
||||
"eslint-plugin-fsd-import": "^0.0.13",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"neverthrow": "^8.1.1",
|
||||
"npm": "^10.8.3",
|
||||
"npm": "^11.7.0",
|
||||
"react": "^18.3.1",
|
||||
"react-bem-helper": "^1.4.1",
|
||||
"react-calendar": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export interface DailyRoutinePluginSettings {
|
|||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: DailyRoutinePluginSettings = {
|
||||
dailyRoutineFolderPath: "DAILY_ROUTINE",
|
||||
dailyRoutineFolderPath: "daily_routine",
|
||||
isMondayStartOfWeek: true,
|
||||
confirmUncheckTask: true
|
||||
}
|
||||
|
|
@ -33,14 +33,13 @@ export class DailyRoutineSettingTab extends PluginSettingTab {
|
|||
new Setting(containerEl)
|
||||
.setName("Daily routine folder path")
|
||||
.setDesc("This is the path to the folder where the Daily Routine Plugin saves notes, routines, and other data.")
|
||||
.addText(text => {
|
||||
new FileSuggest(text.inputEl, "folder");
|
||||
text
|
||||
.setPlaceholder("DAILY_ROUTINE")
|
||||
.setValue(this.plugin.settings.dailyRoutineFolderPath??"")
|
||||
// .onChange(async (value) => {
|
||||
// this.save({ dailyRoutineFolderPath: normalizePath(value)});
|
||||
// })
|
||||
.addText(textComponent => {
|
||||
new FileSuggest(textComponent, "folder")
|
||||
.setPlaceholder("daily_routine")
|
||||
.setDefaultValue(this.plugin.settings.dailyRoutineFolderPath??"")
|
||||
.onChange((value) => {
|
||||
this.updateSettings({ dailyRoutineFolderPath: normalizePath(value)});
|
||||
})
|
||||
})
|
||||
|
||||
// Start of Week
|
||||
|
|
@ -54,9 +53,9 @@ export class DailyRoutineSettingTab extends PluginSettingTab {
|
|||
"sunday": "Sunday"
|
||||
})
|
||||
.setValue(this.plugin.settings.isMondayStartOfWeek ? "monday" : "sunday")
|
||||
.onChange(async (value) => {
|
||||
.onChange((value) => {
|
||||
const isMondayStartOfWeek = value === "monday";
|
||||
this.save({ isMondayStartOfWeek });
|
||||
this.updateSettings({ isMondayStartOfWeek });
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -67,16 +66,23 @@ export class DailyRoutineSettingTab extends PluginSettingTab {
|
|||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.confirmUncheckTask)
|
||||
.onChange(async (value) => {
|
||||
this.save({ confirmUncheckTask: value });
|
||||
.onChange((value) => {
|
||||
this.updateSettings({ confirmUncheckTask: value });
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async save(partial: Partial<DailyRoutinePluginSettings>) {
|
||||
// 닫을 때 저장하기 위해서 override
|
||||
override hide(): void {
|
||||
super.hide();
|
||||
|
||||
// 저장하고 view를 새로고침
|
||||
this.plugin.saveSettings()
|
||||
.then(() => useLeaf.getState().refresh());
|
||||
}
|
||||
|
||||
updateSettings(partial: Partial<DailyRoutinePluginSettings>) {
|
||||
const settings = {...this.plugin.settings, ...partial};
|
||||
this.plugin.settings = {...this.plugin.settings, ...partial};
|
||||
await this.plugin.saveSettings();
|
||||
useLeaf.getState().refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,9 @@ import { RoutineGroupEntity } from "../domain/routine-group";
|
|||
|
||||
|
||||
const parse = async (file: TFile): Promise<RoutineGroup> => {
|
||||
const fm = await fileAccessor.loadFrontMatter(file);
|
||||
const name = file.basename.startsWith(GROUP_PREFIX) ? file.basename.slice(GROUP_PREFIX.length) : file.basename;
|
||||
const properties = RoutineGroupEntity.validateGroupProperties(fileAccessor.loadFrontMatter(file));
|
||||
const properties = RoutineGroupEntity.validateGroupProperties(fm);
|
||||
if(properties.isErr()) throw new Error(`[RoutineGroup '${name}' Parse Error] ${properties.error}`);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,16 @@ import dedent from "dedent";
|
|||
import { Routine } from "../domain/routine-type";
|
||||
|
||||
|
||||
/**
|
||||
* File을 받아서 Routine 객체로 변환한다.
|
||||
*
|
||||
* 루틴을 저장소에서 읽어와 변환하는 로직은 모두 해당 함수를 사용한다.
|
||||
* @param file
|
||||
* @returns
|
||||
*/
|
||||
const parse = async (file: TFile): Promise<Routine> => {
|
||||
const result = RoutineEntity.validateRoutineProperties(fileAccessor.loadFrontMatter(file));
|
||||
const fm = await fileAccessor.loadFrontMatter(file);
|
||||
const result = RoutineEntity.validateRoutineProperties(fm);
|
||||
if(result.isErr()){
|
||||
new Notice(`Routine '${file.basename}' frontmatter error: ${result.error}`);
|
||||
throw new Error(`[Routine '${file.basename}' Parse Error] ${result.error}`);
|
||||
|
|
@ -32,7 +40,7 @@ export interface RoutineQuery {
|
|||
load(routineName: string): Promise<Routine>;
|
||||
}
|
||||
export interface RoutineRepository extends RoutineQuery {
|
||||
persist(entity: Routine): Promise<boolean>;
|
||||
create(entity: Routine): Promise<boolean>;
|
||||
delete(routineName: string): Promise<void>;
|
||||
changeName(originalName: string, newName: string): Promise<void>;
|
||||
update(routine: Routine): Promise<Routine>;
|
||||
|
|
@ -62,7 +70,7 @@ export const routineRepository: RoutineRepository = {
|
|||
return await parse(file);
|
||||
},
|
||||
|
||||
async persist(routine: Routine){
|
||||
async create(routine: Routine){
|
||||
const path = ROUTINE_PATH(routine.name);
|
||||
const file = fileAccessor.loadFile(path);
|
||||
if(!file){
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useCallback, useEffect, useMemo } from 'react';
|
|||
import { ModalApi } from './create-modal';
|
||||
import { TextEditComponent } from "@shared/components/TextEditComponent"
|
||||
import { Button } from "@shared/components/Button"
|
||||
import { plugin } from "@shared/utils/plugin-service-locator";
|
||||
|
||||
|
||||
type ModalProps = {
|
||||
|
|
@ -173,14 +174,22 @@ export const SaveBtn = ({
|
|||
name,
|
||||
}: SaveBtnProps) => {
|
||||
|
||||
const handleEnterKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if(e.key === "Enter" && !disabled) onSaveBtnClick?.();
|
||||
}, [disabled, onSaveBtnClick]);
|
||||
|
||||
/**
|
||||
* Enter Key로 Save Button을 트리거하는 이벤트를 등록
|
||||
*/
|
||||
useEffect(() => {
|
||||
|
||||
const handleEnterKeyDown = ({ key, isComposing}: KeyboardEvent) => {
|
||||
if(isComposing) return;
|
||||
|
||||
if(key === "Enter" && !disabled){
|
||||
onSaveBtnClick?.();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleEnterKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleEnterKeyDown);
|
||||
}, [handleEnterKeyDown]);
|
||||
}, [disabled, onSaveBtnClick]);
|
||||
|
||||
return (
|
||||
<div css={{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { plugin } from "@shared/utils/plugin-service-locator";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
|
||||
import { TFile, TFolder, getFrontMatterInfo, parseYaml } from "obsidian";
|
||||
|
||||
|
||||
export interface FileAccessor {
|
||||
|
|
@ -58,7 +57,7 @@ export interface FileAccessor {
|
|||
*/
|
||||
writeFrontMatter: (file: TFile, frontMatterModifier: (frontmatter: any) => any) => Promise<void>;
|
||||
|
||||
loadFrontMatter: (file: TFile) => object | null;
|
||||
loadFrontMatter: (file: TFile) => Promise<object>;
|
||||
}
|
||||
|
||||
export const fileAccessor: FileAccessor = {
|
||||
|
|
@ -116,8 +115,30 @@ export const fileAccessor: FileAccessor = {
|
|||
});
|
||||
},
|
||||
|
||||
loadFrontMatter: (file: TFile) => {
|
||||
/**
|
||||
* 1차 시도는 metadataCache에서 frontmatter를 가져오고,
|
||||
* 실패하면 파일을 직접 읽어서 frontmatter를 파싱한다.
|
||||
* 그래도 안되면 에러를 발생시킨다.
|
||||
* @param file
|
||||
* @returns
|
||||
*/
|
||||
loadFrontMatter: async (file: TFile): Promise<object> => {
|
||||
const metadataCache = plugin().app.metadataCache.getFileCache(file);
|
||||
return metadataCache?.frontmatter ?? null;
|
||||
if(metadataCache && metadataCache.frontmatter){
|
||||
return metadataCache.frontmatter;
|
||||
} else {
|
||||
const content = await fileAccessor.readFileAsReadonly(file);
|
||||
return parseFrontmatterFromContent(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const parseFrontmatterFromContent = (fileContent: string): object => {
|
||||
const fmInfo = getFrontMatterInfo(fileContent);
|
||||
if(!fmInfo.exists){
|
||||
return {};
|
||||
}
|
||||
const yaml = fmInfo.frontmatter.replace('---', '').trim()
|
||||
return parseYaml(yaml);
|
||||
}
|
||||
|
|
@ -112,15 +112,19 @@ export class Day {
|
|||
}
|
||||
|
||||
get dow(): DayOfWeek {
|
||||
const dayOfWeekNum = this.#moment.format('ddd');
|
||||
let dayOfWeekNum = parseInt(this.#moment.format('d'), 10);
|
||||
// ISO 8601 (Monday = 1) 체계를 사용하는 경우 조정
|
||||
if(this.#moment.localeData().firstDayOfWeek() === 1) {
|
||||
dayOfWeekNum = dayOfWeekNum === 0 ? 6 : dayOfWeekNum - 1; // Sunday(0)으로 맞춤
|
||||
}
|
||||
switch(dayOfWeekNum) {
|
||||
case "Sun": return DayOfWeek.SUN;
|
||||
case "Mon": return DayOfWeek.MON;
|
||||
case "Tue": return DayOfWeek.TUE;
|
||||
case "Wed": return DayOfWeek.WED;
|
||||
case "Thu": return DayOfWeek.THU;
|
||||
case "Fri": return DayOfWeek.FRI;
|
||||
case "Sat": return DayOfWeek.SAT;
|
||||
case 0: return DayOfWeek.SUN;
|
||||
case 1: return DayOfWeek.MON;
|
||||
case 2: return DayOfWeek.TUE;
|
||||
case 3: return DayOfWeek.WED;
|
||||
case 4: return DayOfWeek.THU;
|
||||
case 5: return DayOfWeek.FRI;
|
||||
case 6: return DayOfWeek.SAT;
|
||||
default: throw new Error('Invalid day of week number.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { DR_SETTING } from "@app/settings/setting-provider";
|
||||
import { Day } from "./day";
|
||||
import { Day, DayOfWeek } from "./day";
|
||||
|
||||
|
||||
export class Week {
|
||||
|
|
@ -11,25 +11,30 @@ export class Week {
|
|||
* DR_SETTING.isMondayStartOfWeek()를 통해 월요일부터 시작하는지를 확인하고,
|
||||
* 그에 따라서 startDay와 endDay를 적절히 조정한다.
|
||||
*/
|
||||
constructor(day: Day) {
|
||||
private constructor(startOfDay: Day) {
|
||||
this.#startDay = startOfDay;
|
||||
this.#endDay = startOfDay.clone(m => m.add(6, "day"));
|
||||
}
|
||||
|
||||
static of(day: Day): Week {
|
||||
const isMondayStart = DR_SETTING.isMondayStartOfWeek();
|
||||
const s = day.clone(m => m.startOf("week"));
|
||||
|
||||
// 월요일 시작인데 일요일인 경우(하루 뒤로)
|
||||
if(isMondayStart && s.format("ddd") === "Sun") {
|
||||
this.#startDay = s.clone(m => m.add(1, "day"));
|
||||
let startDay: Day;
|
||||
|
||||
// 월요일 시작인데 일요일인 경우
|
||||
if(isMondayStart && s.dow === DayOfWeek.SUN) {
|
||||
startDay = s.clone(m => m.subtract(6, "day"));
|
||||
}
|
||||
// 일요일 시작인데 월요일인 경우(하루 앞으로)
|
||||
else if(!isMondayStart && s.format("ddd") === "Mon") {
|
||||
this.#startDay = s.clone(m => m.subtract(1, "day"));
|
||||
else if(!isMondayStart && s.dow === DayOfWeek.MON) {
|
||||
startDay = s.clone(m => m.subtract(1, "day"));
|
||||
}
|
||||
// 잘 부합하는 경우
|
||||
else {
|
||||
this.#startDay = s;
|
||||
startDay = s;
|
||||
}
|
||||
|
||||
// endDay는 startDay로부터 6일 뒤로 설정한다.
|
||||
this.#endDay = this.#startDay.clone(m => m.add(6, "day"));
|
||||
return new Week(startDay);
|
||||
}
|
||||
|
||||
get startDay() {
|
||||
|
|
@ -40,19 +45,17 @@ export class Week {
|
|||
return this.#endDay;
|
||||
}
|
||||
|
||||
get weekNum(){
|
||||
get weekNum() {
|
||||
return this.#startDay.week;
|
||||
}
|
||||
|
||||
add_cpy(amount: number) {
|
||||
return new Week(
|
||||
this.#startDay.clone(m => m.add(amount * 7, "day"))
|
||||
);
|
||||
const newStartOfweek = this.#startDay.clone(m => m.add(amount * 7, "day"));
|
||||
return new Week(newStartOfweek);
|
||||
}
|
||||
|
||||
subtract_cpy(amount: number) {
|
||||
return new Week(
|
||||
this.#startDay.clone(m => m.subtract(amount * 7, "day"))
|
||||
);
|
||||
const newStartOfweek = this.#startDay.clone(m => m.subtract(amount * 7, "day"))
|
||||
return new Week(newStartOfweek);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
|
||||
|
||||
import { AbstractInputSuggest, TAbstractFile, TFile, TFolder } from "obsidian";
|
||||
import { AbstractInputSuggest, TAbstractFile, TextComponent, TFile, TFolder } from "obsidian";
|
||||
import { plugin } from "@shared/utils/plugin-service-locator";
|
||||
|
||||
|
||||
export class FileSuggest extends AbstractInputSuggest<TAbstractFile> {
|
||||
#isTypeRight: (file: TAbstractFile) => boolean;
|
||||
|
||||
constructor(private inputEl: HTMLInputElement, type: "file" | "folder") {
|
||||
super(plugin().app, inputEl);
|
||||
/**
|
||||
* TextComponent.setValue()로 변경한 데이터는 onChange 이벤트가 발생하지 않음.
|
||||
* FilSuggest.selectSuggestion()가 호출되고 나서 onChange를 발생시켜줘야함
|
||||
*/
|
||||
#onSelectCbs: ((path: string) => void)[];
|
||||
|
||||
|
||||
constructor(private textComponent: TextComponent, type: "file" | "folder") {
|
||||
super(plugin().app, textComponent.inputEl);
|
||||
|
||||
// File인지 Folder인지 검사하는 함수 초기화
|
||||
this.#isTypeRight =
|
||||
type === "file"
|
||||
?
|
||||
|
|
@ -19,6 +26,10 @@ export class FileSuggest extends AbstractInputSuggest<TAbstractFile> {
|
|||
function (file: TAbstractFile): file is TFolder {
|
||||
return file instanceof TFolder;
|
||||
};
|
||||
|
||||
this.#onSelectCbs = [];
|
||||
|
||||
textComponent.inputEl.blur();
|
||||
}
|
||||
|
||||
getSuggestions(inputStr: string): TAbstractFile[] {
|
||||
|
|
@ -41,9 +52,27 @@ export class FileSuggest extends AbstractInputSuggest<TAbstractFile> {
|
|||
|
||||
override selectSuggestion(value: TAbstractFile, evt: MouseEvent | KeyboardEvent): void {
|
||||
this.setValue(value.path);
|
||||
this.#onSelectCbs.forEach(cb => cb(value.path));
|
||||
this.close();
|
||||
}
|
||||
|
||||
onChange(callback: (value: string) => void): FileSuggest {
|
||||
this.textComponent.onChange(callback);
|
||||
this.#onSelectCbs.push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
override renderSuggestion(file: TAbstractFile, el: HTMLElement): void {
|
||||
el.setText(file.path);
|
||||
}
|
||||
|
||||
setPlaceholder(placeholder: string): FileSuggest {
|
||||
this.textComponent.setPlaceholder(placeholder);
|
||||
return this;
|
||||
}
|
||||
|
||||
setDefaultValue(value: string): FileSuggest {
|
||||
this.textComponent.setValue(value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
|
||||
|
||||
import { ISuggestOwner, Scope, AbstractInputSuggest } from "obsidian";
|
||||
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
|
||||
import { plugin } from "@shared/utils/plugin-service-locator";
|
||||
|
||||
const wrapAround = (value: number, size: number): number => {
|
||||
return ((value % size) + size) % size;
|
||||
};
|
||||
|
||||
class Suggest<T> {
|
||||
private owner: ISuggestOwner<T>;
|
||||
private values: T[];
|
||||
private suggestions: HTMLDivElement[];
|
||||
private selectedItem: number;
|
||||
private containerEl: HTMLElement;
|
||||
|
||||
constructor(
|
||||
owner: ISuggestOwner<T>,
|
||||
containerEl: HTMLElement,
|
||||
scope: Scope
|
||||
) {
|
||||
this.owner = owner;
|
||||
this.containerEl = containerEl;
|
||||
|
||||
containerEl.on(
|
||||
"click",
|
||||
".suggestion-item",
|
||||
this.onSuggestionClick.bind(this)
|
||||
);
|
||||
containerEl.on(
|
||||
"mousemove",
|
||||
".suggestion-item",
|
||||
this.onSuggestionMouseover.bind(this)
|
||||
);
|
||||
|
||||
scope.register([], "ArrowUp", (event) => {
|
||||
if (!event.isComposing) {
|
||||
this.setSelectedItem(this.selectedItem - 1, true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
scope.register([], "ArrowDown", (event) => {
|
||||
if (!event.isComposing) {
|
||||
this.setSelectedItem(this.selectedItem + 1, true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
scope.register([], "Enter", (event) => {
|
||||
if (!event.isComposing) {
|
||||
this.useSelectedItem(event);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
|
||||
event.preventDefault();
|
||||
|
||||
const item = this.suggestions.indexOf(el);
|
||||
this.setSelectedItem(item, false);
|
||||
this.useSelectedItem(event);
|
||||
}
|
||||
|
||||
onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void {
|
||||
const item = this.suggestions.indexOf(el);
|
||||
this.setSelectedItem(item, false);
|
||||
}
|
||||
|
||||
setSuggestions(values: T[]) {
|
||||
this.containerEl.empty();
|
||||
const suggestionEls: HTMLDivElement[] = [];
|
||||
|
||||
values.forEach((value) => {
|
||||
const suggestionEl = this.containerEl.createDiv("suggestion-item");
|
||||
this.owner.renderSuggestion(value, suggestionEl);
|
||||
suggestionEls.push(suggestionEl);
|
||||
});
|
||||
|
||||
this.values = values;
|
||||
this.suggestions = suggestionEls;
|
||||
this.setSelectedItem(0, false);
|
||||
}
|
||||
|
||||
useSelectedItem(event: MouseEvent | KeyboardEvent) {
|
||||
const currentValue = this.values[this.selectedItem];
|
||||
if (currentValue) {
|
||||
this.owner.selectSuggestion(currentValue, event);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
|
||||
const normalizedIndex = wrapAround(
|
||||
selectedIndex,
|
||||
this.suggestions.length
|
||||
);
|
||||
const prevSelectedSuggestion = this.suggestions[this.selectedItem];
|
||||
const selectedSuggestion = this.suggestions[normalizedIndex];
|
||||
|
||||
prevSelectedSuggestion?.removeClass("is-selected");
|
||||
selectedSuggestion?.addClass("is-selected");
|
||||
|
||||
this.selectedItem = normalizedIndex;
|
||||
|
||||
if (scrollIntoView) {
|
||||
selectedSuggestion.scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
|
||||
protected inputEl: HTMLInputElement | HTMLTextAreaElement;
|
||||
|
||||
private popper: PopperInstance;
|
||||
private scope: Scope;
|
||||
private suggestEl: HTMLElement;
|
||||
private suggest: Suggest<T>;
|
||||
|
||||
constructor(inputEl: HTMLInputElement | HTMLTextAreaElement) {
|
||||
this.inputEl = inputEl;
|
||||
this.scope = new Scope();
|
||||
|
||||
this.suggestEl = createDiv("suggestion-container");
|
||||
const suggestion = this.suggestEl.createDiv("suggestion");
|
||||
this.suggest = new Suggest(this, suggestion, this.scope);
|
||||
|
||||
this.scope.register([], "Escape", this.close.bind(this));
|
||||
|
||||
plugin().registerDomEvent(this.inputEl, "input", this.onInputChanged.bind(this));
|
||||
plugin().registerDomEvent(this.inputEl, "focus", this.onInputChanged.bind(this));
|
||||
plugin().registerDomEvent(this.inputEl, "blur", this.close.bind(this));
|
||||
this.suggestEl.on(
|
||||
"mousedown",
|
||||
".suggestion-container",
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onInputChanged(): void {
|
||||
const inputStr = this.inputEl.value;
|
||||
const suggestions = this.getSuggestions(inputStr);
|
||||
|
||||
if(!suggestions) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if(suggestions.length > 0) {
|
||||
this.suggest.setSuggestions(suggestions);
|
||||
this.open(plugin().app.workspace.containerEl, this.inputEl);
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
open(container: HTMLElement, inputEl: HTMLElement): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
plugin().app.keymap.pushScope(this.scope);
|
||||
|
||||
container.appendChild(this.suggestEl);
|
||||
this.popper = createPopper(inputEl, this.suggestEl, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "sameWidth",
|
||||
enabled: true,
|
||||
fn: ({ state, instance }) => {
|
||||
// Note: positioning needs to be calculated twice -
|
||||
// first pass - positioning it according to the width of the popper
|
||||
// second pass - position it with the width bound to the reference element
|
||||
// we need to early exit to avoid an infinite loop
|
||||
const targetWidth = `${state.rects.reference.width}px`;
|
||||
if (state.styles.popper.width === targetWidth) {
|
||||
return;
|
||||
}
|
||||
state.styles.popper.width = targetWidth;
|
||||
instance.update();
|
||||
},
|
||||
phase: "beforeWrite",
|
||||
requires: ["computeStyles"],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
plugin().app.keymap.popScope(this.scope);
|
||||
|
||||
this.suggest.setSuggestions([]);
|
||||
if (this.popper) this.popper.destroy();
|
||||
this.suggestEl.detach();
|
||||
}
|
||||
|
||||
abstract getSuggestions(inputStr: string): T[];
|
||||
abstract renderSuggestion(item: T, el: HTMLElement): void;
|
||||
abstract selectSuggestion(item: T): void;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import BEMHelper, { WordSet } from "react-bem-helper"
|
||||
import BEMHelper, { WordSet } from "react-bem-helper";
|
||||
|
||||
|
||||
export type DailyRoutineBEM = (element?: string, modifiers?: WordSet, extra?: WordSet) => string;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const useStartRoutineModal = createModal(({ modal }: StartRoutineModalPro
|
|||
const [routine, dispatch] = useReducer<RoutineReducer>(routineReducer, createNewRoutine());
|
||||
|
||||
const onSaveBtnClick = useCallback(async () => {
|
||||
await routineRepository.persist(routine);
|
||||
await routineRepository.create(routine);
|
||||
mergeNotes();
|
||||
modal.close();
|
||||
new Notice(`Routine '${routine.name}' started! 🎉`);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ interface WeeksProps {
|
|||
export const WeeksWidget = ({ className }: WeeksProps) => {
|
||||
const { note, setNote } = useRoutineNote();
|
||||
const activeDay = useMemo(() => note.day, [note]);
|
||||
const activeWeek = useMemo(() => new Week(activeDay), [activeDay]);
|
||||
const activeWeek = useMemo(() => Week.of(activeDay), [activeDay]);
|
||||
const currentNotePerformance = useMemo(() => NoteEntity.getPerformance(note), [note]);
|
||||
const [ weeks, setWeeks ] = useState<WeekNode[]>([]);
|
||||
const { leafBgColor } = useLeaf();
|
||||
|
|
@ -40,12 +40,12 @@ export const WeeksWidget = ({ className }: WeeksProps) => {
|
|||
const loadWeekNum = 1;
|
||||
let loadTarget: Week;
|
||||
if(edge === "start"){
|
||||
const currentEdgeWeek = new Week(week.days[0].day);
|
||||
const currentEdgeWeek = Week.of(week.days[0].day);
|
||||
loadTarget = currentEdgeWeek.subtract_cpy(loadWeekNum);
|
||||
return await loadWeekNodes(loadTarget);
|
||||
}
|
||||
else { // edge === "end"
|
||||
const currentEdgeWeek = new Week(week.days[week.days.length-1].day);
|
||||
const currentEdgeWeek = Week.of(week.days[week.days.length-1].day);
|
||||
loadTarget = currentEdgeWeek.add_cpy(loadWeekNum);
|
||||
return await loadWeekNodes(loadTarget);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const loadWeekNodes: LoadWeekNodes = async (week, { prev, next } = { prev
|
|||
const startDay = startWeek.startDay;
|
||||
const endWeek = week.add_cpy(next);
|
||||
const endDay = endWeek.endDay;
|
||||
|
||||
const realNotes = await noteRepository.loadBetween(startDay, endDay);
|
||||
// 일단 실제로 가져온 노트들을 기반으로 dayNodes의 기본 틀을 만든다.
|
||||
const dayNodes: DayNode[] = realNotes.map(note => ({
|
||||
|
|
@ -54,7 +55,7 @@ export const loadWeekNodes: LoadWeekNodes = async (week, { prev, next } = { prev
|
|||
for(const node of dayNodes) {
|
||||
const week = weeks[weekIdx];
|
||||
|
||||
if(week.week === null) week.week = new Week(node.day);
|
||||
if(week.week === null) week.week = Week.of(node.day);
|
||||
week.days.push(node);
|
||||
|
||||
if(week.days.length === 7){
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"1.0.2": "1.8.0"
|
||||
}
|
||||
"1.0.8": "1.8.0"
|
||||
}
|
||||
Loading…
Reference in a new issue