mirror of
https://github.com/poanse/obsidian-taskmap.git
synced 2026-07-22 06:05:58 +00:00
initial commit
This commit is contained in:
parent
e2a64e0534
commit
d90f45b637
28 changed files with 6646 additions and 134 deletions
134
main.ts
134
main.ts
|
|
@ -1,134 +0,0 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
|
||||
// Remember to rename these classes and interfaces!
|
||||
|
||||
interface MyPluginSettings {
|
||||
mySetting: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
mySetting: 'default'
|
||||
}
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (_evt: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
new Notice('This is a notice!');
|
||||
});
|
||||
// Perform additional things with the ribbon
|
||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText('Status Bar Text');
|
||||
|
||||
// This adds a simple command that can be triggered anywhere
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-simple',
|
||||
name: 'Open sample modal (simple)',
|
||||
callback: () => {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
});
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'sample-editor-command',
|
||||
name: 'Sample editor command',
|
||||
editorCallback: (editor: Editor, _view: MarkdownView) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection('Sample Editor Command');
|
||||
}
|
||||
});
|
||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-complex',
|
||||
name: 'Open sample modal (complex)',
|
||||
checkCallback: (checking: boolean) => {
|
||||
// Conditions to check
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
// If checking is true, we're simply "checking" if the command can be run.
|
||||
// If checking is false, then we want to actually perform the operation.
|
||||
if (!checking) {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
|
||||
// This command will only show up in Command Palette when the check function returns true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
console.log('click', evt);
|
||||
});
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class SampleModal extends Modal {
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.setText('Woah!');
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Setting #1')
|
||||
.setDesc('It\'s a secret')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your secret')
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
3703
package-lock.json
generated
Normal file
3703
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
46
qodana.yaml
Normal file
46
qodana.yaml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
|
||||
#################################################################################
|
||||
# WARNING: Do not store sensitive information in this file, #
|
||||
# as its contents will be included in the Qodana report. #
|
||||
#################################################################################
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
|
||||
# severityThresholds - configures maximum thresholds for different problem severities
|
||||
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
|
||||
# Code Coverage is available in Ultimate and Ultimate Plus plans
|
||||
#failureConditions:
|
||||
# severityThresholds:
|
||||
# any: 15
|
||||
# critical: 5
|
||||
# testCoverageThresholds:
|
||||
# fresh: 70
|
||||
# total: 50
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-js:2025.2
|
||||
80
src/LinkSuggest.ts
Normal file
80
src/LinkSuggest.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { App, TFile, AbstractInputSuggest, prepareFuzzySearch } from "obsidian";
|
||||
|
||||
export class LinkSuggest extends AbstractInputSuggest<TFile> {
|
||||
textInputEl: HTMLTextAreaElement;
|
||||
|
||||
constructor(app: App, textInputEl: HTMLTextAreaElement) {
|
||||
// Cast the textarea to HTMLInputElement to satisfy TypeScript.
|
||||
// This is a dirty hack, but runtime behavior is compatible
|
||||
super(app, textInputEl as unknown as HTMLInputElement);
|
||||
this.textInputEl = textInputEl;
|
||||
}
|
||||
|
||||
getSuggestions(query: string): TFile[] {
|
||||
const cursor = this.textInputEl.selectionStart;
|
||||
const text = this.textInputEl.value;
|
||||
|
||||
// Find the start of the current line
|
||||
const lineStart = text.lastIndexOf("\n", cursor - 1) + 1;
|
||||
const textBeforeCursor = text.substring(lineStart, cursor);
|
||||
|
||||
// Regex to match "[[query" at the very end of the text before cursor
|
||||
const match = textBeforeCursor.match(/\[\[([^\]]*)$/);
|
||||
|
||||
// If we are NOT inside a [[link]], return empty array to close/hide suggestions
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We are inside a link! Extract the actual query string
|
||||
const linkQuery = match[1];
|
||||
|
||||
// Perform the search
|
||||
const search = prepareFuzzySearch(linkQuery);
|
||||
const files = this.app.vault.getFiles();
|
||||
|
||||
return files
|
||||
.filter((file) => search(file.path) || search(file.basename))
|
||||
.sort((a, b) => {
|
||||
const resA = search(a.path);
|
||||
const resB = search(b.path);
|
||||
return (resB?.score || 0) - (resA?.score || 0);
|
||||
})
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
renderSuggestion(file: TFile, el: HTMLElement) {
|
||||
el.createEl("div", { text: file.basename });
|
||||
el.createEl("small", { text: file.path, cls: "text-muted" });
|
||||
}
|
||||
|
||||
selectSuggestion(file: TFile) {
|
||||
const cursor = this.textInputEl.selectionStart;
|
||||
const text = this.textInputEl.value;
|
||||
|
||||
const lineStart = text.lastIndexOf("\n", cursor - 1) + 1;
|
||||
const textBeforeCursor = text.substring(lineStart, cursor);
|
||||
const match = textBeforeCursor.match(/\[\[([^\]]*)$/);
|
||||
|
||||
if (!match) return;
|
||||
|
||||
const linkStart = lineStart + match.index!;
|
||||
const beforeLink = text.substring(0, linkStart);
|
||||
const afterCursor = text.substring(cursor);
|
||||
|
||||
const newLinkText = `[[${file.basename}]]`;
|
||||
|
||||
this.textInputEl.value = beforeLink + newLinkText + afterCursor;
|
||||
|
||||
// Trigger Svelte update
|
||||
this.textInputEl.dispatchEvent(new Event("input"));
|
||||
|
||||
// Reset cursor
|
||||
const newCursor = beforeLink.length + newLinkText.length;
|
||||
this.textInputEl.setSelectionRange(newCursor, newCursor);
|
||||
|
||||
this.textInputEl.focus();
|
||||
this.textInputEl.blur();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
7
src/PluginSettings.ts
Normal file
7
src/PluginSettings.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface PluginSettings {
|
||||
mySetting: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: PluginSettings = {
|
||||
mySetting: 'default'
|
||||
}
|
||||
81
src/ProjectData.svelte.ts
Normal file
81
src/ProjectData.svelte.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { StatusCode, type TaskData } from "./types";
|
||||
import TaskmapPlugin from "./main";
|
||||
|
||||
export class ProjectData {
|
||||
tasks = $state(new Array<TaskData>());
|
||||
curTaskId = 0;
|
||||
|
||||
public static getDefault(): ProjectData {
|
||||
return new ProjectData({
|
||||
tasks: new Array<TaskData>(),
|
||||
curTaskId: 0,
|
||||
});
|
||||
}
|
||||
|
||||
public serialize() {
|
||||
return JSON.stringify(
|
||||
{ tasks: this.tasks, curTaskId: this.curTaskId },
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(obj: { tasks: TaskData[]; curTaskId: number }) {
|
||||
this.tasks = obj.tasks;
|
||||
this.curTaskId = obj.curTaskId;
|
||||
if (this.tasks.length == 0) {
|
||||
this.addTask();
|
||||
}
|
||||
}
|
||||
|
||||
public addTask() {
|
||||
const id = this.curTaskId;
|
||||
this.tasks.push({
|
||||
taskId: id,
|
||||
status: StatusCode.DRAFT,
|
||||
name: "default",
|
||||
deleted: false,
|
||||
});
|
||||
this.curTaskId += 1;
|
||||
return id;
|
||||
}
|
||||
|
||||
public removeTask(id: number) {
|
||||
const t = this.getTask(id);
|
||||
t.deleted = true;
|
||||
}
|
||||
|
||||
public getTask(taskId: number) {
|
||||
const res = this.tasks.find((t) => t.taskId == taskId);
|
||||
if (res) {
|
||||
return res!;
|
||||
} else {
|
||||
throw new Error(`No task found with id ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
public isTaskDeleted(taskId: number) {
|
||||
return this.getTask(taskId).deleted;
|
||||
}
|
||||
|
||||
public getTaskStatus(taskId: number) {
|
||||
return this.getTask(taskId)!.status;
|
||||
}
|
||||
public getTaskName(taskId: number) {
|
||||
return this.getTask(taskId)!.name;
|
||||
}
|
||||
public setTaskStatus(taskId: number, status: StatusCode) {
|
||||
const task = this.getTask(taskId);
|
||||
if (task) {
|
||||
task.status = status;
|
||||
}
|
||||
}
|
||||
public setTaskName(taskId: number, name: string) {
|
||||
const task = this.getTask(taskId);
|
||||
if (task) {
|
||||
task.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_DATA = ProjectData.getDefault().serialize();
|
||||
28
src/TaskmapSettingTab.ts
Normal file
28
src/TaskmapSettingTab.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { PluginSettingTab, App, Setting } from "obsidian";
|
||||
import type TaskmapPlugin from "./main";
|
||||
|
||||
export class TaskmapSettingTab extends PluginSettingTab {
|
||||
plugin: TaskmapPlugin;
|
||||
|
||||
constructor(app: App, plugin: TaskmapPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Setting #1')
|
||||
.setDesc('It\'s a secret')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your secret')
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
109
src/TaskmapView.ts
Normal file
109
src/TaskmapView.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { debounce, TextFileView, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { mount } from "svelte";
|
||||
import { DEFAULT_DATA, ProjectData } from "./ProjectData.svelte";
|
||||
import pixi from "./components/pixi.svelte";
|
||||
import { UIState } from "./pixi/GlobalState.svelte";
|
||||
|
||||
export const VIEW_TYPE_EXAMPLE = "example";
|
||||
|
||||
export class TaskmapView extends TextFileView {
|
||||
// raw svelte/html
|
||||
// taskMapViewComponent: ReturnType<typeof TaskmapViewComponent> | undefined;
|
||||
// pixi
|
||||
pixiComponent: ReturnType<typeof pixi> | undefined;
|
||||
// canvas
|
||||
// canvasComponent: ReturnType<typeof DemoApp> | undefined;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
super(leaf);
|
||||
}
|
||||
|
||||
debouncedSave = debounce(() => this.save(), 1000, true);
|
||||
|
||||
data: string = DEFAULT_DATA;
|
||||
projectData: ProjectData;
|
||||
|
||||
getViewType() {
|
||||
return VIEW_TYPE_EXAMPLE;
|
||||
}
|
||||
|
||||
async onLoadFile(file: TFile): Promise<void> {
|
||||
this.file = file;
|
||||
this.setViewData(await this.app.vault.read(file));
|
||||
let projectData = new ProjectData(JSON.parse(this.data));
|
||||
this.projectData = projectData;
|
||||
// this.taskMapViewComponent = mount(TaskmapViewComponent, {
|
||||
// target: this.contentEl,
|
||||
// props: {
|
||||
// taskmapView: this,
|
||||
// },
|
||||
// });
|
||||
// this.canvasComponent = mount(DemoApp, {
|
||||
// target: this.contentEl,
|
||||
// props: {
|
||||
// taskmapView: this,
|
||||
// },
|
||||
// });
|
||||
this.pixiComponent = mount(pixi, {
|
||||
target: this.contentEl,
|
||||
props: {
|
||||
uiState: new UIState(this.projectData, this.app),
|
||||
projectData: this.projectData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getFilePath(): string {
|
||||
if (this.file) {
|
||||
return this.file.path;
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
getViewData(): string {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
setViewData(data: string = DEFAULT_DATA, clear = false): void {
|
||||
this.data = data;
|
||||
|
||||
if (clear) {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
try {
|
||||
await this.app.vault.modify(
|
||||
this.file!,
|
||||
this.projectData.serialize(),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Save failed:", err);
|
||||
throw new Error(`Save failed. ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.debouncedSave.cancel();
|
||||
this.setViewData(DEFAULT_DATA);
|
||||
}
|
||||
|
||||
async onUnloadFile(file: TFile): Promise<void> {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.debouncedSave.cancel();
|
||||
}
|
||||
|
||||
// onChange(value: string) {
|
||||
// this.setViewData(value);
|
||||
// this.debouncedSave();
|
||||
// }
|
||||
}
|
||||
76
src/components/AddTaskButton.svelte
Normal file
76
src/components/AddTaskButton.svelte
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<script lang="ts">
|
||||
import {TASK_SIZE} from "../pixi/Constants";
|
||||
import {StatusCode} from "../types";
|
||||
import {UIState} from "../pixi/GlobalState.svelte";
|
||||
import type {ProjectData} from "../ProjectData.svelte";
|
||||
|
||||
const {
|
||||
taskId,
|
||||
uiState,
|
||||
projectData,
|
||||
}: {
|
||||
taskId: number,
|
||||
uiState: UIState,
|
||||
projectData: ProjectData,
|
||||
} = $props();
|
||||
|
||||
let taskData = $derived(projectData.getTask(taskId));
|
||||
|
||||
let entered = $state(false);
|
||||
function onEnter() {
|
||||
entered = true;
|
||||
console.log('enter');
|
||||
}
|
||||
function onLeave() {
|
||||
entered = false;
|
||||
console.log('leave');
|
||||
}
|
||||
function addButtonPressed (event: MouseEvent) {
|
||||
console.log('add icon clicked')
|
||||
uiState.addTask();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="hover-area"
|
||||
onmouseenter={onEnter}
|
||||
onmouseleave={onLeave}
|
||||
style="
|
||||
position: absolute;
|
||||
left: {TASK_SIZE.width - 50/2}px;
|
||||
top: {TASK_SIZE.height/2 - 50/2}px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 2px dashed #888;
|
||||
background: transparent;
|
||||
pointer-events: auto;
|
||||
"
|
||||
>
|
||||
</div>
|
||||
{#if entered}
|
||||
<svg
|
||||
class="button-add"
|
||||
class:draft={taskData.status === StatusCode.DRAFT}
|
||||
class:ready={taskData.status === StatusCode.READY}
|
||||
class:in-progress={taskData.status === StatusCode.IN_PROGRESS}
|
||||
class:done={taskData.status === StatusCode.DONE}
|
||||
onmouseenter={onEnter}
|
||||
onmouseleave={onLeave}
|
||||
onclick={addButtonPressed}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
|
||||
style="
|
||||
position: absolute;
|
||||
left: {TASK_SIZE.width - 41/2 - 1}px;
|
||||
top: {TASK_SIZE.height/2 - 41/2}px;
|
||||
width: 41;
|
||||
height: 41;
|
||||
"
|
||||
>
|
||||
<circle
|
||||
cx="12" cy="12" r="10"
|
||||
/>
|
||||
<path d="M5 12h14"/><path d="M12 5v14"/>
|
||||
</svg>
|
||||
{/if}
|
||||
86
src/components/Button.svelte
Normal file
86
src/components/Button.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
import type {UIState} from "../pixi/GlobalState.svelte.js";
|
||||
import {IconCode, StatusCode} from "../types";
|
||||
import IconService from "../pixi/IconService";
|
||||
|
||||
let {
|
||||
iconCode,
|
||||
uiState,
|
||||
}: {
|
||||
iconCode: IconCode,
|
||||
uiState: UIState,
|
||||
} = $props();
|
||||
|
||||
let isPressedDown = $state(false);
|
||||
let isPressed=$derived(uiState.pressedButtonIndex == iconCode);
|
||||
|
||||
const stateful = iconCode != IconCode.FOCUS;
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
isPressedDown = true;
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
isPressedDown = false;
|
||||
}
|
||||
|
||||
function handleMouseUp(event: MouseEvent) {
|
||||
if (!isPressedDown) {
|
||||
return;
|
||||
}
|
||||
isPressedDown = false;
|
||||
if (isPressed) {
|
||||
isPressed = false;
|
||||
uiState.pressedButtonIndex = -1;
|
||||
} else if (stateful) {
|
||||
isPressed = true;
|
||||
uiState.pressedButtonIndex = iconCode;
|
||||
} else {
|
||||
uiState.pressedButtonIndex = -1;
|
||||
}
|
||||
let newStatus: StatusCode | null = null;
|
||||
if (iconCode == IconCode.STATUS_READY) {
|
||||
newStatus = StatusCode.READY;
|
||||
} else if (iconCode == IconCode.STATUS_DRAFT) {
|
||||
newStatus = StatusCode.DRAFT;
|
||||
} else if (iconCode == IconCode.STATUS_DONE) {
|
||||
newStatus = StatusCode.DONE;
|
||||
} else if (iconCode == IconCode.STATUS_IN_PROGRESS) {
|
||||
newStatus = StatusCode.IN_PROGRESS;
|
||||
}
|
||||
if (newStatus != null) {
|
||||
uiState.changeStatus(newStatus);
|
||||
}
|
||||
if (iconCode == IconCode.TRASH_SINGLE || iconCode == IconCode.TRASH_MULTIPLE) {
|
||||
uiState.removeTask(uiState.selectedTaskId);
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function _getIcon() {
|
||||
if (iconCode == IconCode.STATUS) {
|
||||
return IconService.getStatusIcon(uiState.toolbarStatus);
|
||||
}
|
||||
return IconService.getIcon(iconCode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="button"
|
||||
class:no-pan={true}
|
||||
class:is-pressed-up={isPressed}
|
||||
class:is-pressed-down={isPressedDown}
|
||||
onmousedown={handleMouseDown}
|
||||
onmouseup={handleMouseUp}
|
||||
onmouseleave={onBlur}
|
||||
onclick={(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
if (IconCode.FOCUS === iconCode) {
|
||||
uiState.setSelectedTaskId(-1);
|
||||
}
|
||||
}}
|
||||
role="presentation"
|
||||
tabindex="-1"
|
||||
>
|
||||
{@html _getIcon()}
|
||||
</div>
|
||||
204
src/components/Task.svelte
Normal file
204
src/components/Task.svelte
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
<script lang="ts">
|
||||
import {UIState} from "../pixi/GlobalState.svelte.js";
|
||||
import type {ProjectData} from "../ProjectData.svelte.js";
|
||||
import {StatusCode} from "../types";
|
||||
import TaskText from "./TaskText.svelte";
|
||||
import AddTaskButton from "./AddTaskButton.svelte";
|
||||
|
||||
const {
|
||||
taskId,
|
||||
uiState,
|
||||
projectData,
|
||||
coords
|
||||
}: {
|
||||
taskId: number,
|
||||
uiState: UIState,
|
||||
projectData: ProjectData,
|
||||
coords: {x: number, y: number}
|
||||
} = $props();
|
||||
|
||||
let taskData = $derived(projectData.getTask(taskId));
|
||||
// let coords = $derived(uiState.getTaskPosition(taskId));
|
||||
|
||||
let isHovered = $state(false);
|
||||
// derived here is a must
|
||||
let isSelected = $derived(uiState.isSelected(taskData.taskId));
|
||||
let editing = $state(false);
|
||||
let inputValue = $state(taskData.name);
|
||||
const useNewTextElement = true;
|
||||
|
||||
function startEditing() {
|
||||
editing = true;
|
||||
inputValue = taskData.name;
|
||||
}
|
||||
|
||||
function finishEditing(success: boolean) {
|
||||
if (!useNewTextElement) {
|
||||
if (success && inputValue.trim() !== "") {
|
||||
taskData.name = inputValue.trim();
|
||||
uiState.save();
|
||||
}
|
||||
inputValue = taskData.name;
|
||||
editing = false;
|
||||
}
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges()
|
||||
}
|
||||
// const input = (document.getElementById('titleInput') as HTMLInputElement);
|
||||
// input.selectionStart = 0;
|
||||
// input.selectionEnd = 1;
|
||||
// input.disabled = true;
|
||||
}
|
||||
|
||||
function textInputHandleKey(e: KeyboardEvent) {
|
||||
console.log('textInputHandleKey ', e.key);
|
||||
if (!editing) {
|
||||
return;
|
||||
}
|
||||
console.log('textInputHandleKey handled', e.key);
|
||||
if (e.key === "Enter") {
|
||||
finishEditing(true);
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
finishEditing(false);
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let mouseDown = $state(false);
|
||||
function onTaskClick(event: Event) {
|
||||
console.log(`Task clicked ${taskId}`);
|
||||
if (!mouseDown) {
|
||||
return;
|
||||
}
|
||||
mouseDown = false;
|
||||
if (editing) {
|
||||
finishEditing(true);
|
||||
} else {
|
||||
uiState.setSelectedTaskId(taskData.taskId);
|
||||
// const input = (document.getElementById('titleInput') as HTMLInputElement);
|
||||
// input.disabled = false;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function onTextClick(event: Event) {
|
||||
console.log('TextClicked');
|
||||
if (!isSelected) {
|
||||
uiState.setSelectedTaskId(taskData.taskId);
|
||||
// const input = (document.getElementById('titleInput') as HTMLInputElement);
|
||||
// input.disabled = false;
|
||||
} else if (editing == false) {
|
||||
startEditing();
|
||||
}
|
||||
// } else if (editing) {
|
||||
// finishEditing(true);
|
||||
// }
|
||||
event.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if !projectData.isTaskDeleted(taskId)}
|
||||
<div
|
||||
class="task-container"
|
||||
style="
|
||||
top: {coords.y}px;
|
||||
left: {coords.x}px;
|
||||
position: absolute;
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="task"
|
||||
class:no-pan={true}
|
||||
class:draft={taskData.status === StatusCode.DRAFT}
|
||||
class:ready={taskData.status === StatusCode.READY}
|
||||
class:in-progress={taskData.status === StatusCode.IN_PROGRESS}
|
||||
class:done={taskData.status === StatusCode.DONE}
|
||||
onmouseenter={() => isHovered = true}
|
||||
onmouseleave={() => isHovered = false}
|
||||
onmousedown={(event: MouseEvent) => {
|
||||
mouseDown = true;
|
||||
event.stopPropagation();
|
||||
}}
|
||||
class:hovered={isHovered || isSelected}
|
||||
onclick={onTaskClick}
|
||||
onblur={() => finishEditing(true)}
|
||||
role="presentation"
|
||||
>
|
||||
{#if useNewTextElement}
|
||||
<TaskText
|
||||
{taskId}
|
||||
{uiState}
|
||||
app={uiState.app}
|
||||
content={taskData.name}
|
||||
onSave={(newContent)=> {
|
||||
taskData.name = newContent;
|
||||
uiState.save();
|
||||
}}
|
||||
sourcePath={uiState.getActiveView().getFilePath()}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
class="textInput"
|
||||
class:no-pan={true}
|
||||
type="text"
|
||||
id="titleInput"
|
||||
bind:value={inputValue}
|
||||
onclick={onTextClick}
|
||||
onblur={() => finishEditing(true)}
|
||||
onmousedown={(e: MouseEvent) => e.stopPropagation()}
|
||||
onkeydown={textInputHandleKey}
|
||||
disabled={!isSelected}
|
||||
style='{ !isSelected? "pointer-events: none;": ""}'
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<AddTaskButton {uiState} {projectData} {taskId} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--top: {coords.y}px;-->
|
||||
<!--left: {coords.x}px;-->
|
||||
|
||||
<!--style="-->
|
||||
<!--position: absolute;-->
|
||||
<!--top: {coords.y + task.shiftY + task.height /2 - fontSize/2 - 7}px;-->
|
||||
<!--left: {coords.x + task.shiftX + task.strokeWidthSelected-2}px;-->
|
||||
<!--"-->
|
||||
|
||||
<!--onclick={onTextClick}-->
|
||||
|
||||
|
||||
<!--<div-->
|
||||
<!-- class="taskText"-->
|
||||
<!-- onclick={onTextClick}-->
|
||||
<!-- onblur={() => finishEditing(true)}-->
|
||||
<!-- role="presentation"-->
|
||||
<!-->-->
|
||||
<!-- <input-->
|
||||
<!-- class="textInput"-->
|
||||
<!-- type="text"-->
|
||||
<!-- id="titleInput"-->
|
||||
<!-- bind:value={inputValue}-->
|
||||
<!-- onkeydown={textInputHandleKey}-->
|
||||
<!-- disabled={!isSelected}-->
|
||||
<!-- style='{ !isSelected? "pointer-events: none;": ""}'-->
|
||||
<!-- />-->
|
||||
<!--</div>-->
|
||||
|
||||
|
||||
<!--{#if isSelected}-->
|
||||
<!-- <input-->
|
||||
<!-- class="textInput"-->
|
||||
<!-- type="text"-->
|
||||
<!-- id="titleInput"-->
|
||||
<!-- bind:value={inputValue}-->
|
||||
<!-- onkeydown={handleKey}-->
|
||||
<!-- />-->
|
||||
<!--{:else}-->
|
||||
<!-- {taskData.name}-->
|
||||
<!--{/if}-->
|
||||
155
src/components/TaskText.svelte
Normal file
155
src/components/TaskText.svelte
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<script lang="ts">
|
||||
import { onMount, tick } from "svelte";
|
||||
import {MarkdownRenderer, Component, App} from "obsidian";
|
||||
import {LinkSuggest} from "../LinkSuggest";
|
||||
import type {UIState} from "../pixi/GlobalState.svelte";
|
||||
|
||||
// PROPS
|
||||
let {
|
||||
taskId,
|
||||
uiState,
|
||||
app,
|
||||
content,
|
||||
sourcePath,
|
||||
onSave
|
||||
}: {
|
||||
taskId: number,
|
||||
uiState: UIState,
|
||||
app: App;
|
||||
content: string;
|
||||
sourcePath: string;
|
||||
onSave: (newContent: string) => void
|
||||
} = $props();
|
||||
|
||||
// STATE
|
||||
let isEditing = $state(false);
|
||||
let suggest: LinkSuggest | null = null;
|
||||
let textPreviewEl: HTMLElement;
|
||||
let textEditEl: HTMLTextAreaElement;
|
||||
let component = new Component(); // Required by Obsidian to manage render lifecycle
|
||||
|
||||
onMount(() => {
|
||||
if(!isEditing && textPreviewEl) {
|
||||
renderMarkdown();
|
||||
}
|
||||
return () => {
|
||||
// Clean up Obsidian component references to prevent memory leaks
|
||||
component.unload();
|
||||
};
|
||||
});
|
||||
$effect(() => {
|
||||
if (!isEditing && textPreviewEl) {
|
||||
renderMarkdown();
|
||||
}
|
||||
});
|
||||
function handlePreviewClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if the clicked element (or its parent) is an internal link
|
||||
const internalLink = target.closest(".internal-link");
|
||||
if (internalLink) {
|
||||
// Prevent the default "Swap to Edit" behavior
|
||||
e.preventDefault(); // TODO: is this line needed?
|
||||
e.stopPropagation();
|
||||
|
||||
const href = internalLink.getAttribute("data-href");
|
||||
if (href === null) {
|
||||
throw new Error('href is null');
|
||||
}
|
||||
|
||||
// Open the link using Obsidian's API
|
||||
// Whether to open in a new tab or in current one
|
||||
const openInNewLeaf = e.ctrlKey || e.metaKey;
|
||||
|
||||
app.workspace.openLinkText(href, sourcePath, openInNewLeaf);
|
||||
return;
|
||||
}
|
||||
|
||||
const externalLink = target.closest("a.external-link");
|
||||
if (externalLink){
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (uiState.isSelected(taskId)) {
|
||||
toggleEdit();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreviewMouseOver(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
const link = target.closest(".internal-link");
|
||||
|
||||
if (link) {
|
||||
// Trigger the native hover preview
|
||||
app.workspace.trigger("hover-link", {
|
||||
event: e,
|
||||
source: "preview",
|
||||
hoverParent: textPreviewEl,
|
||||
targetEl: link,
|
||||
linktext: link.getAttribute("data-href"),
|
||||
sourcePath: sourcePath
|
||||
});
|
||||
}
|
||||
}
|
||||
async function renderMarkdown() {
|
||||
textPreviewEl.empty(); // Clear previous render
|
||||
await MarkdownRenderer.render(
|
||||
app,
|
||||
content,
|
||||
textPreviewEl,
|
||||
sourcePath,
|
||||
component
|
||||
);
|
||||
}
|
||||
|
||||
async function toggleEdit() {
|
||||
isEditing = true;
|
||||
// Wait for DOM update so textarea exists, then focus
|
||||
await tick();
|
||||
if (textEditEl) {
|
||||
textEditEl.focus();
|
||||
suggest = new LinkSuggest(app, textEditEl);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleBlur() {
|
||||
isEditing = false;
|
||||
onSave(content); // Persist changes
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
textEditEl.blur(); // Triggers handleBlur
|
||||
} else if (e.key === "Tab" && suggest !== null) {
|
||||
// another hack to select suggest on tab
|
||||
e.preventDefault();
|
||||
textEditEl.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="task-text-container" onclick={handlePreviewClick}>
|
||||
{#if isEditing}
|
||||
<textarea
|
||||
class="text-edit tasktext"
|
||||
maxlength="28"
|
||||
bind:this={textEditEl}
|
||||
bind:value={content}
|
||||
onblur={handleBlur}
|
||||
onkeydown={handleKeydown}
|
||||
></textarea>
|
||||
{:else}
|
||||
<div
|
||||
class="text-preview tasktext"
|
||||
tabindex="0"
|
||||
bind:this={textPreviewEl}
|
||||
onclick={handlePreviewClick}
|
||||
onmouseover={handlePreviewMouseOver}
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!--onkeydown={(e) => e.key === 'Enter' && toggleEdit()}-->
|
||||
80
src/components/Toolbar.svelte
Normal file
80
src/components/Toolbar.svelte
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<script lang="ts">
|
||||
import Button from "./Button.svelte";
|
||||
import { fade } from 'svelte/transition';
|
||||
import {
|
||||
TOOLBAR_PADDING,
|
||||
TOOLBAR_GAP,
|
||||
BUTTON_SIZE,
|
||||
TOOLBAR_SIZE, TASK_SIZE, TOOLBAR_SHIFT, SUBTOOLBAR_SHIFT
|
||||
} from "../pixi/Constants";
|
||||
// 1. Import the transition you want
|
||||
import {quintOut} from 'svelte/easing';
|
||||
import {slideCustom} from '../pixi/Custom';
|
||||
import type {UIState} from "../pixi/GlobalState.svelte.js";
|
||||
import {IconCode} from "../types";
|
||||
|
||||
let {
|
||||
uiState
|
||||
}: {uiState: UIState} = $props();
|
||||
|
||||
let position = $derived(uiState.getCurrentTaskPosition(uiState.selectedTaskId));
|
||||
|
||||
function getTop() {
|
||||
return position.y - TOOLBAR_SIZE.height - TOOLBAR_SHIFT;
|
||||
}
|
||||
function getLeft() {
|
||||
return position.x + TASK_SIZE.width_hovered / 2 - TOOLBAR_SIZE.width / 2;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="toolbar"
|
||||
class:no-pan={true}
|
||||
in:fade={{ duration: 500 }}
|
||||
out:fade={{ duration: 300 }}
|
||||
style="
|
||||
top: {getTop()}px;
|
||||
left: {getLeft()}px;
|
||||
"
|
||||
>
|
||||
{#key uiState.updateOnZoomCounter}
|
||||
<Button iconCode={IconCode.TRASH} {uiState}/>
|
||||
<Button iconCode={IconCode.KEY} {uiState} />
|
||||
<Button iconCode={IconCode.LOCK} {uiState} />
|
||||
<Button iconCode={IconCode.FOCUS} {uiState} />
|
||||
<Button iconCode={IconCode.STATUS} {uiState} />
|
||||
{/key}
|
||||
</div>
|
||||
{#if uiState.pressedButtonIndex === IconCode.TRASH}
|
||||
<div
|
||||
class="subtoolbar"
|
||||
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
|
||||
style="
|
||||
top: {getTop() - 2 * BUTTON_SIZE - TOOLBAR_GAP - 2 * TOOLBAR_PADDING.y - SUBTOOLBAR_SHIFT}px;
|
||||
left: {getLeft()}px;
|
||||
"
|
||||
>
|
||||
{#key uiState.updateOnZoomCounter}
|
||||
<Button iconCode={IconCode.TRASH_SINGLE} {uiState} />
|
||||
<Button iconCode={IconCode.TRASH_MULTIPLE} {uiState} />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiState.pressedButtonIndex === IconCode.STATUS}
|
||||
<div
|
||||
class="subtoolbar"
|
||||
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
|
||||
style="
|
||||
top: {getTop() - 4 * BUTTON_SIZE - 3*TOOLBAR_GAP - 2*TOOLBAR_PADDING.y - SUBTOOLBAR_SHIFT}px;
|
||||
left: {getLeft() + 4 * (BUTTON_SIZE + TOOLBAR_GAP)}px;
|
||||
"
|
||||
>
|
||||
{#key uiState.updateOnZoomCounter}
|
||||
<Button iconCode={IconCode.STATUS_DRAFT} {uiState} />
|
||||
<Button iconCode={IconCode.STATUS_READY} {uiState} />
|
||||
<Button iconCode={IconCode.STATUS_IN_PROGRESS} {uiState} />
|
||||
<Button iconCode={IconCode.STATUS_DONE} {uiState} />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
137
src/components/pixi.svelte
Normal file
137
src/components/pixi.svelte
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<script lang="ts">
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
import Toolbar from "./Toolbar.svelte";
|
||||
import Task from "./Task.svelte";
|
||||
import Panzoom, {type PanzoomObject} from '@panzoom/panzoom'
|
||||
import type {Context} from "../types";
|
||||
|
||||
let {uiState, projectData}: Context = $props();
|
||||
|
||||
let viewportEl: HTMLDivElement | null = null;
|
||||
let sceneEl: HTMLDivElement | null = null;
|
||||
let panzoom: PanzoomObject | null = null;
|
||||
let isDragging = false;
|
||||
let mouseDown = $state(false);
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
|
||||
|
||||
// Cleanup listeners to prevent memory leaks
|
||||
onDestroy(() => {
|
||||
if (viewportEl && panzoom) {
|
||||
viewportEl.removeEventListener('wheel', panzoom.zoomWithWheel);
|
||||
}
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (!sceneEl) {
|
||||
throw new Error('No mount element');
|
||||
}
|
||||
if (!viewportEl) {
|
||||
throw new Error('No viewport');
|
||||
}
|
||||
|
||||
// 1. Initialize Panzoom on the child (scene), NOT the wrapper
|
||||
panzoom = Panzoom(sceneEl, {
|
||||
maxScale: 3, // not higher than 1 to avoid lowRes svg rendering
|
||||
minScale: 0.1,
|
||||
// Important: Use the wrapper as the bounds for event handling
|
||||
// This fixes the "Infinite" panning issue because the parent catches the events
|
||||
canvas: true,
|
||||
excludeClass: 'no-pan',
|
||||
cursor: 'default',
|
||||
// animate: true,
|
||||
});
|
||||
|
||||
// 2. Fix Zoom: Manually bind the wheel event to the viewport
|
||||
viewportEl.addEventListener('wheel',
|
||||
(ev) => {
|
||||
getPanzoom().zoomWithWheel(ev);
|
||||
if (getPanzoom().getScale() > 1) {
|
||||
uiState.incrementUpdateOnZoomCounter();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 3. Fix Panning from "empty space": Bind down event to viewport
|
||||
// This ensures you can drag even if the element is off-center
|
||||
viewportEl.addEventListener('pointerdown', (e) => {
|
||||
getPanzoom().handleDown(e);
|
||||
|
||||
// Track click vs drag
|
||||
isDragging = false;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
});
|
||||
});
|
||||
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
console.log('handleKey ', e.key);
|
||||
if (e.key === "Escape") {
|
||||
uiState.setSelectedTaskId(-1);
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
function handlePointerUp(e: PointerEvent) {
|
||||
// Calculate distance moved
|
||||
const dist = Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2);
|
||||
// If moved more than 5 pixels, it's a drag, not a click
|
||||
const thrDist = 5;
|
||||
if (dist > Math.pow(thrDist,2)) {
|
||||
isDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCanvasClick(e: MouseEvent) {
|
||||
// 4. Fix Click: Only trigger logic if we haven't dragged
|
||||
if (isDragging) {
|
||||
e.stopPropagation(); // Stop propagation if it was a drag
|
||||
return;
|
||||
}
|
||||
console.log('Canvas clicked!', e);
|
||||
if (!mouseDown) {
|
||||
return
|
||||
}
|
||||
mouseDown = false;
|
||||
console.log(`Window clicked + ${uiState.serializeForDebugging()}`);
|
||||
console.log('selectedTaskId ' + uiState.selectedTaskId);
|
||||
uiState.pressedButtonIndex = -1;
|
||||
uiState.setSelectedTaskId(-1);
|
||||
sceneEl!.focus();
|
||||
}
|
||||
function getPanzoom() {
|
||||
if (panzoom) {
|
||||
return panzoom;
|
||||
} else {
|
||||
throw new Error('No panzoom element');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="viewport"
|
||||
bind:this={viewportEl}
|
||||
onpointerup={handlePointerUp}
|
||||
style="width: 100%; height: 100vh; overflow: hidden; position: relative; background: #1C1C1C;"
|
||||
>
|
||||
<div
|
||||
class="pixi-container"
|
||||
bind:this={sceneEl}
|
||||
onpointerdown={() => mouseDown = true}
|
||||
onclick={handleCanvasClick}
|
||||
tabindex="-1"
|
||||
onkeydown={handleKey}
|
||||
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;"
|
||||
role="presentation"
|
||||
>
|
||||
{#each projectData.tasks.filter(t => !t.deleted) as task}
|
||||
<Task taskId={task.taskId} {uiState} {projectData} coords={uiState.getCurrentTaskPosition(task.taskId)} />
|
||||
{/each}
|
||||
|
||||
{#if uiState.selectedTaskId !== -1}
|
||||
<Toolbar
|
||||
uiState={uiState}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
135
src/legacy/Button.ts
Normal file
135
src/legacy/Button.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import {
|
||||
type Application,
|
||||
Color,
|
||||
Container,
|
||||
Graphics,
|
||||
SCALE_MODES,
|
||||
type Size,
|
||||
Sprite,
|
||||
Texture,
|
||||
} from "pixi.js";
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
export enum ButtonState {
|
||||
INACTIVE = "INACTIVE",
|
||||
INACTIVE_HOVERED = "INACTIVE_HOVERED",
|
||||
ACTIVE = "ACTIVE",
|
||||
ACTIVE_HOVERED = "ACTIVE_HOVERED",
|
||||
// TODO: pressed
|
||||
}
|
||||
|
||||
export type ColorStateMap = {
|
||||
[key in ButtonState]: Color;
|
||||
};
|
||||
|
||||
class ButtonOptions {
|
||||
size: Size;
|
||||
backgroundColors: ColorStateMap;
|
||||
iconColors: ColorStateMap;
|
||||
}
|
||||
|
||||
export class Button extends Container {
|
||||
private app: Application;
|
||||
private state: ButtonState;
|
||||
private opts: ButtonOptions;
|
||||
private iconSource: Promise<Graphics | Texture>;
|
||||
|
||||
private background: Sprite;
|
||||
private icon: Sprite;
|
||||
|
||||
constructor(
|
||||
app: Application,
|
||||
initialState: ButtonState,
|
||||
iconSource: Promise<Graphics | Texture>,
|
||||
opts: ButtonOptions,
|
||||
) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.state = initialState;
|
||||
this.iconSource = iconSource;
|
||||
this.opts = opts;
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.eventMode = "static";
|
||||
this.cursor = "pointer";
|
||||
|
||||
this.on("pointerover", this.hoverOn);
|
||||
this.on("pointerout", this.hoverOff);
|
||||
this.on("pointerdown", () => {
|
||||
console.log("Clicked icon");
|
||||
});
|
||||
// background
|
||||
|
||||
// tooltip
|
||||
// ...
|
||||
this.update();
|
||||
// callbacks
|
||||
// ...
|
||||
}
|
||||
|
||||
async update() {
|
||||
// TODO: я идиот и менял не тот бэкграунд
|
||||
// this.background.destroy();
|
||||
|
||||
const bgTexture = this.app.renderer.generateTexture({
|
||||
target: new Graphics()
|
||||
.roundRect(0, 0, this.opts.size.width, this.opts.size.height, 2)
|
||||
.fill({ color: this.opts.backgroundColors[this.state] }),
|
||||
// resolution: 4,
|
||||
// antialias: true,
|
||||
});
|
||||
bgTexture.source.scaleMode = "linear";
|
||||
this.background = new Sprite(bgTexture);
|
||||
this.addChild(this.background);
|
||||
|
||||
const icon = await this.iconSource;
|
||||
const iconTexture = match(icon)
|
||||
.returnType<Texture>()
|
||||
.with(P.instanceOf(Graphics), () =>
|
||||
this.app.renderer.generateTexture({
|
||||
target: icon as Graphics,
|
||||
// .fill({
|
||||
// color: this.opts.iconColors[this.state],
|
||||
// })
|
||||
// antialias: true,
|
||||
resolution: 4,
|
||||
}),
|
||||
)
|
||||
.with(P.instanceOf(Texture), () => icon as Texture)
|
||||
.exhaustive();
|
||||
iconTexture.source.scaleMode = "linear";
|
||||
this.icon = new Sprite({
|
||||
texture: iconTexture,
|
||||
anchor: 0.5,
|
||||
// roundPixels: true,
|
||||
});
|
||||
this.icon.scale = 0.6;
|
||||
this.icon.position.set(
|
||||
this.opts.size.width / 2,
|
||||
this.opts.size.height / 2,
|
||||
);
|
||||
this.addChild(this.icon);
|
||||
}
|
||||
|
||||
changeState(state: ButtonState) {
|
||||
this.state = state;
|
||||
this.icon.destroy();
|
||||
this.update();
|
||||
}
|
||||
|
||||
hoverOn() {
|
||||
if (this.state == ButtonState.ACTIVE) {
|
||||
this.changeState(ButtonState.ACTIVE_HOVERED);
|
||||
} else if (this.state == ButtonState.INACTIVE) {
|
||||
this.changeState(ButtonState.INACTIVE_HOVERED);
|
||||
}
|
||||
}
|
||||
hoverOff() {
|
||||
if (this.state == ButtonState.ACTIVE_HOVERED) {
|
||||
this.changeState(ButtonState.ACTIVE);
|
||||
} else if (this.state == ButtonState.INACTIVE_HOVERED) {
|
||||
this.changeState(ButtonState.INACTIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
219
src/legacy/TaskView.ts
Normal file
219
src/legacy/TaskView.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import { Application, Color, Container, Graphics, TextStyle } from "pixi.js";
|
||||
import TextInput from "./text-input";
|
||||
import { Button, ButtonState } from "./Button";
|
||||
import { alphaBlend } from "@napolab/alpha-blend";
|
||||
import { IconService } from "../pixi/IconService";
|
||||
|
||||
export interface TaskCardOptions {
|
||||
label: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const COLOR_WHITE = "#ffffff"; // Double-click detection
|
||||
const DOUBLE_CLICK_DELAY = 300; // milliseconds
|
||||
let lastClickTime = 0;
|
||||
|
||||
export const TASK_BACKGROUND_DEFAULT_COLOR: Color = new Color("0x1E1E1E");
|
||||
const TASK_BORDER_DEFAULT_COLOR = new Color("0x343434");
|
||||
// const TASK_BACKGROUND_DEFAULT_COLOR: Color = new Color(COLOR_WHITE);
|
||||
const ICON_BACKGROUND_COLOR = new Color("0x1E1E1E");
|
||||
const HOVERED_ICON_BACKGROUND_COLOR = new Color("0x343434");
|
||||
const TOOLBAR_BACKGROUND_DEFAULT_COLOR: string = alphaBlend(
|
||||
ICON_BACKGROUND_COLOR.toRgbaString(),
|
||||
new Color("#000000").setAlpha(0.5).toRgbaString(),
|
||||
);
|
||||
console.log("TASK BACKGROUND COLOR: ", TOOLBAR_BACKGROUND_DEFAULT_COLOR);
|
||||
export const TASK_DEFAULT_BORDER = {
|
||||
color: 0x7e7e7e,
|
||||
width: 1,
|
||||
};
|
||||
export const TASK_FOCUSED_BORDER = {
|
||||
color: 0x7e7e7e,
|
||||
width: 2,
|
||||
};
|
||||
export const TEXT_COLOR = COLOR_WHITE;
|
||||
export const TEXT_STYLE = new TextStyle({
|
||||
fill: TEXT_COLOR,
|
||||
fontSize: 10,
|
||||
fontFamily: "Segoe UI",
|
||||
lineHeight: 1.5,
|
||||
});
|
||||
|
||||
export class TaskView extends Container {
|
||||
private toolbar!: Container;
|
||||
private card!: Graphics;
|
||||
// private text!: Text;
|
||||
private app!: Application;
|
||||
private readonly opts: TaskCardOptions;
|
||||
|
||||
private inputEl: TextInput | null = null;
|
||||
private mountEl: HTMLDivElement;
|
||||
|
||||
constructor(
|
||||
app: Application,
|
||||
mountEl: HTMLDivElement,
|
||||
opts: TaskCardOptions,
|
||||
) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.mountEl = mountEl;
|
||||
this.opts = opts;
|
||||
}
|
||||
|
||||
async draw() {
|
||||
// this.card = new Graphics()
|
||||
// .roundRect(0, yShift, CARD_W, CARD_H, 10)
|
||||
// .fill({ color: TASK_BACKGROUND_DEFAULT_COLOR });
|
||||
// .stroke(TASK_DEFAULT_BORDER)
|
||||
// this.addChild(this.card);
|
||||
|
||||
// TODO: почему-то поле с редактированием текста рисуется на бэкграунде и его не видно
|
||||
// TODO: после редктирования показываются оба варианта текста...
|
||||
let input = new TextInput(this.mountEl, {
|
||||
text: this.opts.label,
|
||||
input: {
|
||||
// multiline: true,
|
||||
fontFamily: TEXT_STYLE.fontFamily,
|
||||
fontSize: `${TEXT_STYLE.fontSize}pt`,
|
||||
padding: `${TEXT_STYLE.padding}px`,
|
||||
width: `${this.opts.width}px`,
|
||||
height: `${this.opts.height}px`,
|
||||
color: TEXT_STYLE.fill,
|
||||
textAlign: "center",
|
||||
// dom params
|
||||
"border-radius": 0,
|
||||
// "background-color": COLOR_WHITE,
|
||||
// "font-size": `${TEXT_STYLE.fontSize}pt`,
|
||||
border: "0px solid #ccc",
|
||||
"box-shadow": "none",
|
||||
// outline: "none",
|
||||
},
|
||||
box: {
|
||||
default: {
|
||||
fill: TOOLBAR_BACKGROUND_DEFAULT_COLOR,
|
||||
rounded: 10,
|
||||
stroke: TASK_DEFAULT_BORDER,
|
||||
},
|
||||
focused: {
|
||||
fill: TOOLBAR_BACKGROUND_DEFAULT_COLOR,
|
||||
rounded: 10,
|
||||
stroke: TASK_FOCUSED_BORDER,
|
||||
},
|
||||
disabled: {
|
||||
fill: TOOLBAR_BACKGROUND_DEFAULT_COLOR,
|
||||
rounded: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
input.placeholder = this.opts.label;
|
||||
// Background
|
||||
// input.x = 100;
|
||||
// input.y = yShift;
|
||||
// input.eventMode = "static";
|
||||
// input.on("blur", () => this.finishEditing());
|
||||
// input.on("pointerdown", (e) => {
|
||||
// const now = Date.now();
|
||||
// if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
|
||||
// this.startEditing();
|
||||
// }
|
||||
// lastClickTime = now;
|
||||
// });
|
||||
this.addChild(input);
|
||||
this.inputEl = input;
|
||||
this.inputEl.render(this.app.renderer);
|
||||
// this.text = new Text({
|
||||
// text: opts.label,
|
||||
// resolution: 4,
|
||||
// style: new TextStyle({
|
||||
// fill: COLOR_WHITE,
|
||||
// fontSize: 10,
|
||||
// fontFamily: "Segoe UI",
|
||||
// lineHeight: 1.5,
|
||||
// }),
|
||||
// });
|
||||
// this.text.anchor.set(0.5);
|
||||
// this.text.position = {
|
||||
// x: CARD_W / 2,
|
||||
// y: 50 + CARD_H / 2,
|
||||
// };
|
||||
// this.text.eventMode = "static";
|
||||
// this.text.on("pointerdown", (e) => {
|
||||
// const now = Date.now();
|
||||
// if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
|
||||
// this.startEditing();
|
||||
// }
|
||||
// lastClickTime = now;
|
||||
// });
|
||||
// this.addChild(this.text);
|
||||
|
||||
// const TB_W = 98;
|
||||
// const TB_H = 22;
|
||||
// const TB_PADDING = { x: 2, y: 2 };
|
||||
// const TB_GAP = 1;
|
||||
//
|
||||
// this.toolbar = new Container();
|
||||
// this.toolbar.position = { x: (CARD_W - TB_W) / 2, y: TB_H };
|
||||
// this.toolbar.addChild(
|
||||
// new Graphics()
|
||||
// .roundRect(0, 0, TB_W, TB_H, 4)
|
||||
// .fill({ color: TOOLBAR_BACKGROUND_DEFAULT_COLOR }),
|
||||
// );
|
||||
// this.addChild(this.toolbar);
|
||||
// // =========================
|
||||
// // 4) Create icons as vector shapes
|
||||
// // =========================
|
||||
//
|
||||
// const icons = [
|
||||
// IconService.makeTrashIcon(),
|
||||
// IconService.makeKeyIcon(),
|
||||
// IconService.makeLockIcon(),
|
||||
// IconService.makeFocusIcon(),
|
||||
// IconService.makeCircleIcon(),
|
||||
// ];
|
||||
//
|
||||
// icons.forEach((icon, i) => {
|
||||
// const button = new Button(this.app, ButtonState.ACTIVE, icon, {
|
||||
// size: { width: 18, height: 18 },
|
||||
// backgroundColors: {
|
||||
// [ButtonState.ACTIVE]: new Color(
|
||||
// TOOLBAR_BACKGROUND_DEFAULT_COLOR,
|
||||
// ),
|
||||
// [ButtonState.ACTIVE_HOVERED]: HOVERED_ICON_BACKGROUND_COLOR,
|
||||
// [ButtonState.INACTIVE]: new Color(ICON_BACKGROUND_COLOR),
|
||||
// [ButtonState.INACTIVE_HOVERED]:
|
||||
// HOVERED_ICON_BACKGROUND_COLOR,
|
||||
// },
|
||||
// iconColors: {
|
||||
// [ButtonState.ACTIVE]: TASK_BACKGROUND_DEFAULT_COLOR,
|
||||
// [ButtonState.ACTIVE_HOVERED]: TASK_BACKGROUND_DEFAULT_COLOR,
|
||||
// [ButtonState.INACTIVE]: TASK_BACKGROUND_DEFAULT_COLOR,
|
||||
// [ButtonState.INACTIVE_HOVERED]:
|
||||
// TASK_BACKGROUND_DEFAULT_COLOR,
|
||||
// },
|
||||
// });
|
||||
// button.x = TB_PADDING.x + i * (18 + TB_GAP);
|
||||
// button.y = TB_PADDING.y;
|
||||
//
|
||||
// button.setup();
|
||||
// this.toolbar.addChild(button);
|
||||
// });
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// LABEL EDIT MODE (HTML input overlay)
|
||||
// ==================================================
|
||||
|
||||
// private startEditing() {
|
||||
// if (this.inputEl?.visible) return;
|
||||
// this.inputEl!.visible = true;
|
||||
// this.inputEl?.focus();
|
||||
// }
|
||||
//
|
||||
// private finishEditing() {
|
||||
// if (!this.inputEl?.visible) return;
|
||||
//
|
||||
// this.text.text = this.inputEl.text;
|
||||
// this.inputEl.visible = false;
|
||||
// }
|
||||
}
|
||||
15
src/legacy/TaskmapViewComponent.svelte
Normal file
15
src/legacy/TaskmapViewComponent.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import type {TaskmapView} from "../TaskmapView";
|
||||
import Task from "../components/Task.svelte";
|
||||
|
||||
let {
|
||||
taskmapView
|
||||
}: {
|
||||
taskmapView: TaskmapView;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p>Hello world!</p>
|
||||
<Task taskData={taskmapView.projectData.taskData} taskmapView={taskmapView} />
|
||||
</div>
|
||||
70
src/legacy/canvas/Ball.svelte
Normal file
70
src/legacy/canvas/Ball.svelte
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import { Layer } from 'svelte-canvas';
|
||||
import { Spring } from 'svelte/motion';
|
||||
|
||||
let { x, y, color, onclick } = $props();
|
||||
|
||||
let dragging = $state(false);
|
||||
|
||||
let _x = $state();
|
||||
let _y = $state();
|
||||
|
||||
const radius = new Spring(80, { stiffness: 0.15, damping: 0.2 });
|
||||
|
||||
const setup = ({ width, height }) => {
|
||||
_x = new Spring(width * x, { stiffness: 0.15, damping: 0.2 });
|
||||
_y = new Spring(height * y, { stiffness: 0.15, damping: 0.2 });
|
||||
};
|
||||
|
||||
const render = ({ context }) => {
|
||||
context.globalCompositeOperation = 'screen';
|
||||
context.fillStyle = color;
|
||||
context.lineWidth = 10;
|
||||
context.beginPath();
|
||||
context.arc($_x, $_y, $radius, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
const onEnter = () => {
|
||||
document.body.style.cursor = 'pointer';
|
||||
radius.set(90);
|
||||
};
|
||||
|
||||
const onLeave = () => {
|
||||
document.body.style.cursor = 'auto';
|
||||
dragging = false;
|
||||
radius.set(80);
|
||||
};
|
||||
|
||||
const onDown = (e) => {
|
||||
dragging = true;
|
||||
radius.set(120);
|
||||
onclick?.();
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
dragging = false;
|
||||
radius.set(80);
|
||||
};
|
||||
|
||||
const onMove = ({ x, y }) => {
|
||||
if (dragging) {
|
||||
_x.set(x);
|
||||
_y.set(y);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Layer
|
||||
{setup}
|
||||
{render}
|
||||
onmouseenter={onEnter}
|
||||
onmouseleave={onLeave}
|
||||
onmousedown={onDown}
|
||||
onmousemove={onMove}
|
||||
onmouseup={onUp}
|
||||
ontouchstart={onDown}
|
||||
ontouchmove={onMove}
|
||||
ontouchend={onUp}
|
||||
/>
|
||||
22
src/legacy/canvas/CanvasApp.svelte
Normal file
22
src/legacy/canvas/CanvasApp.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { Canvas } from 'svelte-canvas';
|
||||
import Ball from './Ball.svelte';
|
||||
|
||||
let balls = [
|
||||
{ color: 'tomato', x: 0.5, y: 0.333 },
|
||||
{ color: 'goldenrod', x: 0.333, y: 0.625 },
|
||||
{ color: 'mediumturquoise', x: 0.667, y: 0.625 },
|
||||
];
|
||||
|
||||
const reorder = (color) => {
|
||||
balls = balls
|
||||
.filter((c) => c.color !== color)
|
||||
.concat(balls.filter((c) => c.color === color));
|
||||
};
|
||||
</script>
|
||||
|
||||
<Canvas layerEvents style="touch-action: none">
|
||||
{#each balls as { color, x, y } (color)}
|
||||
<Ball {color} {x} {y} onclick={() => reorder(color)} />
|
||||
{/each}
|
||||
</Canvas>
|
||||
203
src/legacy/demo/DemoApp.svelte
Normal file
203
src/legacy/demo/DemoApp.svelte
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<script lang="ts">
|
||||
import {tick} from "svelte";
|
||||
import {TEXT_COLOR, TEXT_STYLE} from "../TaskView";
|
||||
import {match, P} from "ts-pattern";
|
||||
|
||||
let coords = $state({ x: 150, y: 150 });
|
||||
let dragging = $state(false);
|
||||
let taskClicked = $state(false);
|
||||
let editing = $state(false);
|
||||
const maxLength = 20;
|
||||
const taskNameEditSize = 20;
|
||||
const fontSize = 20;
|
||||
const fontFamily = match(TEXT_STYLE.fontFamily)
|
||||
.returnType<string>()
|
||||
.with(P.string, (str) => str)
|
||||
.with(P.array<string>(), (ar)=> ar[0])
|
||||
.exhaustive()
|
||||
;
|
||||
|
||||
let textInput = $state('Task 1');
|
||||
let title = $state("Task 1");
|
||||
|
||||
let task = $state({
|
||||
width: 240,
|
||||
height: 80,
|
||||
shiftX: 2,
|
||||
shiftY: 50,
|
||||
rx: 30,
|
||||
strokeWidthSelected: 4,
|
||||
strokeWidthDefault: 2,
|
||||
});
|
||||
let toolbar = $state({
|
||||
width: 100,
|
||||
height: 30,
|
||||
shiftX: 0,
|
||||
shiftY: 0,
|
||||
});
|
||||
toolbar.shiftX = (task.width - toolbar.width) / 2;
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (dragging) {
|
||||
coords = { x: e.layerX, y: e.layerY };
|
||||
}
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
} else if (editing) {
|
||||
finishEditing(true)
|
||||
} else if (taskClicked) {
|
||||
taskClicked = false;
|
||||
}
|
||||
};
|
||||
const onMousedown = () => {
|
||||
dragging = true;
|
||||
};
|
||||
const onTaskClick = () => {
|
||||
if (!taskClicked) {
|
||||
taskClicked = true;
|
||||
} else if (!editing) {
|
||||
taskClicked = false;
|
||||
}
|
||||
};
|
||||
const onTextClick = () => {
|
||||
if (!taskClicked) {
|
||||
taskClicked = true;
|
||||
} else if (!editing) {
|
||||
editing = true;
|
||||
textInput = title;
|
||||
tick().then(() => {
|
||||
const el = document.getElementById("titleInput");
|
||||
if (el) el.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const taskStrokeWidth = () => {
|
||||
return taskClicked ? task.strokeWidthSelected : task.strokeWidthDefault;
|
||||
};
|
||||
|
||||
function finishEditing(success: boolean) {
|
||||
if (success && textInput.trim() !== "") {
|
||||
title = textInput.trim();
|
||||
// taskmapView.debouncedSave();
|
||||
}
|
||||
textInput = title;
|
||||
editing = false;
|
||||
}
|
||||
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") finishEditing(true);
|
||||
if (e.key === "Escape") finishEditing(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svg class="Canvas"
|
||||
width="100%"
|
||||
height="100%"
|
||||
onmousemove={onMouseMove}
|
||||
onmouseup={onMouseUp}
|
||||
role="presentation"
|
||||
>
|
||||
<svg class="Task"
|
||||
role="presentation"
|
||||
width="{task.width + task.shiftX + 2 * task.strokeWidthSelected}px"
|
||||
height="{task.height + task.shiftY + 2 *task.strokeWidthSelected}px"
|
||||
x={coords.x}
|
||||
y={coords.y}
|
||||
>
|
||||
<rect class="taskBg"
|
||||
onmousedown={onMousedown}
|
||||
onclick={onTaskClick}
|
||||
width={task.width}
|
||||
height={task.height}
|
||||
x="{task.shiftX}px"
|
||||
y="{task.shiftY}px"
|
||||
rx={task.rx}
|
||||
fill=#ff3e00
|
||||
stroke-width="{taskStrokeWidth()}"
|
||||
stroke="white"
|
||||
role="presentation"
|
||||
>
|
||||
</rect>
|
||||
|
||||
<text
|
||||
onmousedown={onMousedown}
|
||||
onclick={onTextClick}
|
||||
x={task.width / 2 + task.shiftX}
|
||||
y={task.height / 2 + task.shiftY}
|
||||
fill={TEXT_COLOR}
|
||||
stroke={TEXT_COLOR}
|
||||
font-weight="normal"
|
||||
stroke-width="1px"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="middle"
|
||||
role="presentation"
|
||||
font-size="{fontSize}px"
|
||||
font-family="{fontFamily}"
|
||||
>
|
||||
{title}
|
||||
</text>
|
||||
|
||||
{#if taskClicked}
|
||||
<rect class="toolbarBg"
|
||||
x={toolbar.shiftX}
|
||||
y={toolbar.shiftY}
|
||||
width={toolbar.width}
|
||||
height={toolbar.height}
|
||||
>
|
||||
</rect>
|
||||
{/if}
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
{#if editing}
|
||||
<div
|
||||
class="textEditDiv"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
style="
|
||||
position: absolute;
|
||||
top: {coords.y + task.shiftY + task.height /2 - fontSize/2 - 7}px;
|
||||
left: {coords.x + task.shiftX + task.strokeWidthSelected-2}px;
|
||||
"
|
||||
>
|
||||
<input
|
||||
id="titleInput"
|
||||
bind:value={textInput}
|
||||
onkeydown={handleKey}
|
||||
size={taskNameEditSize}
|
||||
maxlength={maxLength}
|
||||
style="
|
||||
font-family: {fontFamily};
|
||||
font-size: {fontSize}px;
|
||||
color: {TEXT_COLOR};
|
||||
background-color: #ff3e00;
|
||||
border: 0px;
|
||||
outline-width: 0px;
|
||||
padding-left: 5px;
|
||||
text-align: center;
|
||||
/*dominant-baseline: middle;*/
|
||||
/*text-anchor: middle;*/
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--onblur={() => finishEditing(true)}-->
|
||||
|
||||
|
||||
<style>
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
/*right: 200px;*/
|
||||
top: 0;
|
||||
/*bottom: 100px;*/
|
||||
}
|
||||
.textEditDiv {
|
||||
position: absolute;
|
||||
top: 500px;
|
||||
left: 500px;
|
||||
}
|
||||
</style>
|
||||
720
src/legacy/text-input.js
Normal file
720
src/legacy/text-input.js
Normal file
|
|
@ -0,0 +1,720 @@
|
|||
import { Container, Graphics, Text, TextStyle } from "pixi.js";
|
||||
|
||||
export default class TextInput extends Container {
|
||||
constructor(mountEl, styles) {
|
||||
super();
|
||||
this.mountEl = mountEl;
|
||||
this._input_style = Object.assign(
|
||||
{
|
||||
position: "absolute",
|
||||
background: "none",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
transformOrigin: "0 0",
|
||||
lineHeight: "1",
|
||||
},
|
||||
styles.input,
|
||||
);
|
||||
|
||||
if (styles.box)
|
||||
this._box_generator =
|
||||
typeof styles.box === "function"
|
||||
? styles.box
|
||||
: new DefaultBoxGenerator(styles.box);
|
||||
else this._box_generator = null;
|
||||
|
||||
if (this._input_style.hasOwnProperty("multiline")) {
|
||||
this._multiline = !!this._input_style.multiline;
|
||||
delete this._input_style.multiline;
|
||||
} else this._multiline = false;
|
||||
|
||||
this._box_cache = {};
|
||||
this._previous = {};
|
||||
this._dom_added = false;
|
||||
this._dom_visible = true;
|
||||
// Ставим поейсхолдер, но он не использутся если непуст dominput. А он заполняется парой строчек ниже
|
||||
this._placeholder = styles["text"];
|
||||
this._placeholderColor = styles["input"]["color"];
|
||||
this._selection = [0, 0];
|
||||
this._restrict_value = "";
|
||||
this._createDOMInput();
|
||||
this._dom_input.value = styles["text"];
|
||||
this.substituteText = true;
|
||||
this._setState("DEFAULT");
|
||||
this._addListeners();
|
||||
//
|
||||
// this.worldAlpha = 1;
|
||||
this.worldVisible = true;
|
||||
}
|
||||
|
||||
// GETTERS & SETTERS
|
||||
|
||||
get substituteText() {
|
||||
return this._substituted;
|
||||
}
|
||||
|
||||
set substituteText(substitute) {
|
||||
if (this._substituted == substitute) return;
|
||||
|
||||
this._substituted = substitute;
|
||||
|
||||
if (substitute) {
|
||||
this._createSurrogate();
|
||||
this._dom_visible = false;
|
||||
} else {
|
||||
this._destroySurrogate();
|
||||
this._dom_visible = true;
|
||||
}
|
||||
this.placeholder = this._placeholder;
|
||||
this._update();
|
||||
}
|
||||
|
||||
get placeholder() {
|
||||
return this._placeholder;
|
||||
}
|
||||
|
||||
set placeholder(text) {
|
||||
this._placeholder = text;
|
||||
if (this._substituted) {
|
||||
this._updateSurrogate();
|
||||
this._dom_input.placeholder = "";
|
||||
} else {
|
||||
this._dom_input.placeholder = text;
|
||||
}
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(disabled) {
|
||||
this._disabled = disabled;
|
||||
this._dom_input.disabled = disabled;
|
||||
this._setState(disabled ? "DISABLED" : "DEFAULT");
|
||||
}
|
||||
|
||||
get maxLength() {
|
||||
return this._max_length;
|
||||
}
|
||||
|
||||
set maxLength(length) {
|
||||
this._max_length = length;
|
||||
this._dom_input.setAttribute("maxlength", length);
|
||||
}
|
||||
|
||||
get restrict() {
|
||||
return this._restrict_regex;
|
||||
}
|
||||
|
||||
set restrict(regex) {
|
||||
if (regex instanceof RegExp) {
|
||||
regex = regex.toString().slice(1, -1);
|
||||
|
||||
if (regex.charAt(0) !== "^") regex = "^" + regex;
|
||||
|
||||
if (regex.charAt(regex.length - 1) !== "$") regex = regex + "$";
|
||||
|
||||
regex = new RegExp(regex);
|
||||
} else {
|
||||
regex = new RegExp("^[" + regex + "]*$");
|
||||
}
|
||||
|
||||
this._restrict_regex = regex;
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this._dom_input.value;
|
||||
}
|
||||
|
||||
set text(text) {
|
||||
this._dom_input.value = text;
|
||||
if (this._substituted) this._updateSurrogate();
|
||||
}
|
||||
|
||||
get htmlInput() {
|
||||
return this._dom_input;
|
||||
}
|
||||
|
||||
focus() {
|
||||
console.log("focus called");
|
||||
if (this._substituted && !this.dom_visible) {
|
||||
console.log("dom input visible: true");
|
||||
this._setDOMInputVisible(true);
|
||||
}
|
||||
|
||||
this._dom_input.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this._dom_input.blur();
|
||||
}
|
||||
|
||||
select() {
|
||||
this.focus();
|
||||
this._dom_input.select();
|
||||
}
|
||||
|
||||
setInputStyle(key, value) {
|
||||
this._input_style[key] = value;
|
||||
this._dom_input.style[key] = value;
|
||||
|
||||
if (this._substituted && (key === "fontFamily" || key === "fontSize"))
|
||||
this._updateFontMetrics();
|
||||
|
||||
if (this._last_renderer) this._update();
|
||||
}
|
||||
|
||||
destroy(options) {
|
||||
this._destroyBoxCache();
|
||||
super.destroy(options);
|
||||
}
|
||||
|
||||
// SETUP
|
||||
|
||||
_createDOMInput() {
|
||||
if (this._multiline) {
|
||||
this._dom_input = document.createElement("textarea");
|
||||
this._dom_input.style.resize = "none";
|
||||
} else {
|
||||
this._dom_input = document.createElement("input");
|
||||
this._dom_input.type = "text";
|
||||
}
|
||||
|
||||
for (let key in this._input_style) {
|
||||
this._dom_input.style[key] = this._input_style[key];
|
||||
}
|
||||
}
|
||||
|
||||
_addListeners() {
|
||||
this.on("added", this._onAdded.bind(this));
|
||||
this.on("removed", this._onRemoved.bind(this));
|
||||
this._dom_input.addEventListener(
|
||||
"keydown",
|
||||
this._onInputKeyDown.bind(this),
|
||||
);
|
||||
this._dom_input.addEventListener(
|
||||
"input",
|
||||
this._onInputInput.bind(this),
|
||||
);
|
||||
this._dom_input.addEventListener(
|
||||
"keyup",
|
||||
this._onInputKeyUp.bind(this),
|
||||
);
|
||||
this._dom_input.addEventListener("focus", this._onFocused.bind(this));
|
||||
this._dom_input.addEventListener("blur", this._onBlurred.bind(this));
|
||||
}
|
||||
|
||||
_onInputKeyDown(e) {
|
||||
this._selection = [
|
||||
this._dom_input.selectionStart,
|
||||
this._dom_input.selectionEnd,
|
||||
];
|
||||
|
||||
this.emit("keydown", e.keyCode);
|
||||
}
|
||||
|
||||
_onInputInput(e) {
|
||||
if (this._restrict_regex) this._applyRestriction();
|
||||
|
||||
if (this._substituted) this._updateSubstitution();
|
||||
|
||||
this.emit("input", this.text);
|
||||
}
|
||||
|
||||
_onInputKeyUp(e) {
|
||||
this.emit("keyup", e.keyCode);
|
||||
}
|
||||
|
||||
_onFocused() {
|
||||
this._setState("FOCUSED");
|
||||
this.emit("focus");
|
||||
}
|
||||
|
||||
_onBlurred() {
|
||||
this._setState("DEFAULT");
|
||||
this.emit("blur");
|
||||
}
|
||||
|
||||
_onAdded() {
|
||||
this.mountEl.appendChild(this._dom_input);
|
||||
// document.body.appendChild(this._dom_input);
|
||||
this._dom_input.style.display = "none";
|
||||
this._dom_added = true;
|
||||
}
|
||||
|
||||
_onRemoved() {
|
||||
this.mountEl.removeChild(this._dom_input);
|
||||
// document.body.removeChild(this._dom_input);
|
||||
this._dom_added = false;
|
||||
}
|
||||
|
||||
_setState(state) {
|
||||
this.state = state;
|
||||
this._updateBox();
|
||||
if (this._substituted) this._updateSubstitution();
|
||||
}
|
||||
|
||||
// RENDER & UPDATE
|
||||
|
||||
render(renderer) {
|
||||
// super.render(renderer);
|
||||
this._resolution = renderer.resolution;
|
||||
this._last_renderer = renderer;
|
||||
this._canvas_bounds = this._getCanvasBounds();
|
||||
if (this._needsUpdate()) this._update();
|
||||
}
|
||||
|
||||
_update() {
|
||||
this._updateDOMInput();
|
||||
if (this._substituted) this._updateSurrogate();
|
||||
this._updateBox();
|
||||
}
|
||||
|
||||
_updateBox() {
|
||||
if (!this._box_generator) return;
|
||||
|
||||
if (this._needsNewBoxCache()) this._buildBoxCache();
|
||||
|
||||
if (
|
||||
this.state == this._previous.state &&
|
||||
this._box == this._box_cache[this.state]
|
||||
)
|
||||
return;
|
||||
|
||||
if (this._box) this.removeChild(this._box);
|
||||
|
||||
this._box = this._box_cache[this.state];
|
||||
this.addChildAt(this._box, 0);
|
||||
this._previous.state = this.state;
|
||||
}
|
||||
|
||||
_updateSubstitution() {
|
||||
if (this.state === "FOCUSED") {
|
||||
this._dom_visible = true;
|
||||
this._surrogate.visible = this.text.length === 0;
|
||||
} else {
|
||||
this._dom_visible = false;
|
||||
this._surrogate.visible = true;
|
||||
}
|
||||
this._updateDOMInput();
|
||||
this._updateSurrogate();
|
||||
}
|
||||
|
||||
_updateDOMInput() {
|
||||
if (!this._canvas_bounds) {
|
||||
console.log("No Canvas Bounds");
|
||||
return;
|
||||
}
|
||||
|
||||
this._dom_input.style.top = (this._canvas_bounds.top || 0) + "px";
|
||||
this._dom_input.style.left = (this._canvas_bounds.left || 0) + "px";
|
||||
var transform = this._getDOMRelativeWorldTransform();
|
||||
// !!!!
|
||||
// transform.tx += 10;
|
||||
// transform.ty += 50;
|
||||
this._dom_input.style.transform = this._pixiMatrixToCSS(transform);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
top: this._dom_input.style.top,
|
||||
left: this._dom_input.style.left,
|
||||
transform: this._dom_input.style.transform,
|
||||
}),
|
||||
);
|
||||
this._dom_input.style.opacity = this.worldAlpha;
|
||||
console.log("Alpha", this.worldAlpha);
|
||||
this._setDOMInputVisible(this.worldVisible && this._dom_visible);
|
||||
console.log("world visible", this.worldVisible);
|
||||
|
||||
this._previous.canvas_bounds = this._canvas_bounds;
|
||||
this._previous.world_transform = this.worldTransform.clone();
|
||||
this._previous.world_alpha = this.worldAlpha;
|
||||
this._previous.world_visible = this.worldVisible;
|
||||
}
|
||||
|
||||
_applyRestriction() {
|
||||
if (this._restrict_regex.test(this.text)) {
|
||||
this._restrict_value = this.text;
|
||||
} else {
|
||||
this.text = this._restrict_value;
|
||||
this._dom_input.setSelectionRange(
|
||||
this._selection[0],
|
||||
this._selection[1],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// STATE COMPAIRSON (FOR PERFORMANCE BENEFITS)
|
||||
|
||||
_needsUpdate() {
|
||||
return (
|
||||
!this._comparePixiMatrices(
|
||||
this.worldTransform,
|
||||
this._previous.world_transform,
|
||||
) ||
|
||||
!this._compareClientRects(
|
||||
this._canvas_bounds,
|
||||
this._previous.canvas_bounds,
|
||||
) ||
|
||||
this.worldAlpha != this._previous.world_alpha ||
|
||||
this.worldVisible != this._previous.world_visible
|
||||
);
|
||||
}
|
||||
|
||||
_needsNewBoxCache() {
|
||||
let input_bounds = this._getDOMInputBounds();
|
||||
return (
|
||||
!this._previous.input_bounds ||
|
||||
input_bounds.width != this._previous.input_bounds.width ||
|
||||
input_bounds.height != this._previous.input_bounds.height
|
||||
);
|
||||
}
|
||||
|
||||
// INPUT SUBSTITUTION
|
||||
|
||||
_createSurrogate() {
|
||||
this._surrogate_hitbox = new Graphics();
|
||||
this._surrogate_hitbox.alpha = 0;
|
||||
this._surrogate_hitbox.interactive = true;
|
||||
this._surrogate_hitbox.cursor = "text";
|
||||
this._surrogate_hitbox.on(
|
||||
"pointerdown",
|
||||
this._onSurrogateFocus.bind(this),
|
||||
);
|
||||
this.addChild(this._surrogate_hitbox);
|
||||
|
||||
this._surrogate_mask = new Graphics();
|
||||
this.addChild(this._surrogate_mask);
|
||||
|
||||
this._surrogate = new Text({
|
||||
text: "",
|
||||
resolution: 8,
|
||||
});
|
||||
this.addChild(this._surrogate);
|
||||
|
||||
this._surrogate.mask = this._surrogate_mask;
|
||||
|
||||
this._updateFontMetrics();
|
||||
this._updateSurrogate();
|
||||
}
|
||||
|
||||
_updateSurrogate() {
|
||||
let padding = this._deriveSurrogatePadding();
|
||||
let input_bounds = this._getDOMInputBounds();
|
||||
|
||||
this._surrogate.style = this._deriveSurrogateStyle();
|
||||
this._surrogate.style.padding = Math.max.apply(Math, padding);
|
||||
this._surrogate.y = this._multiline
|
||||
? padding[0]
|
||||
: (input_bounds.height - this._surrogate.height) / 2;
|
||||
this._surrogate.x = padding[3];
|
||||
this._surrogate.text = this._deriveSurrogateText();
|
||||
|
||||
switch (this._surrogate.style.align) {
|
||||
case "left":
|
||||
this._surrogate.x = padding[3];
|
||||
break;
|
||||
|
||||
case "center":
|
||||
this._surrogate.x =
|
||||
input_bounds.width * 0.5 - this._surrogate.width * 0.5;
|
||||
break;
|
||||
|
||||
case "right":
|
||||
this._surrogate.x =
|
||||
input_bounds.width - padding[1] - this._surrogate.width;
|
||||
break;
|
||||
}
|
||||
|
||||
this._updateSurrogateHitbox(input_bounds);
|
||||
this._updateSurrogateMask(input_bounds, padding);
|
||||
}
|
||||
|
||||
_updateSurrogateHitbox(bounds) {
|
||||
this._surrogate_hitbox.clear();
|
||||
this._surrogate_hitbox.beginFill(0);
|
||||
this._surrogate_hitbox.drawRect(0, 0, bounds.width, bounds.height);
|
||||
this._surrogate_hitbox.endFill();
|
||||
this._surrogate_hitbox.interactive = !this._disabled;
|
||||
}
|
||||
|
||||
_updateSurrogateMask(bounds, padding) {
|
||||
this._surrogate_mask.clear();
|
||||
this._surrogate_mask.beginFill(0);
|
||||
this._surrogate_mask.drawRect(
|
||||
padding[3],
|
||||
0,
|
||||
bounds.width - padding[3] - padding[1],
|
||||
bounds.height,
|
||||
);
|
||||
this._surrogate_mask.endFill();
|
||||
}
|
||||
|
||||
_destroySurrogate() {
|
||||
if (!this._surrogate) return;
|
||||
|
||||
this.removeChild(this._surrogate);
|
||||
this.removeChild(this._surrogate_mask);
|
||||
this.removeChild(this._surrogate_hitbox);
|
||||
|
||||
this._surrogate.destroy();
|
||||
this._surrogate_hitbox.destroy();
|
||||
|
||||
this._surrogate = null;
|
||||
this._surrogate_hitbox = null;
|
||||
}
|
||||
|
||||
_onSurrogateFocus() {
|
||||
this._setDOMInputVisible(true);
|
||||
//sometimes the input is not being focused by the mouseclick
|
||||
setTimeout(this._ensureFocus.bind(this), 10);
|
||||
}
|
||||
|
||||
_ensureFocus() {
|
||||
if (!this._hasFocus()) this.focus();
|
||||
}
|
||||
|
||||
_deriveSurrogateStyle() {
|
||||
let style = new TextStyle();
|
||||
|
||||
for (var key in this._input_style) {
|
||||
switch (key) {
|
||||
case "color":
|
||||
style.fill = this._input_style.color;
|
||||
break;
|
||||
|
||||
case "fontFamily":
|
||||
case "fontSize":
|
||||
case "fontWeight":
|
||||
case "fontVariant":
|
||||
case "fontStyle":
|
||||
style[key] = this._input_style[key];
|
||||
break;
|
||||
|
||||
case "letterSpacing":
|
||||
style.letterSpacing = parseFloat(
|
||||
this._input_style.letterSpacing,
|
||||
);
|
||||
break;
|
||||
|
||||
case "textAlign":
|
||||
style.align = this._input_style.textAlign;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._multiline) {
|
||||
style.lineHeight = parseFloat(style.fontSize);
|
||||
style.wordWrap = true;
|
||||
style.wordWrapWidth = this._getDOMInputBounds().width;
|
||||
}
|
||||
|
||||
if (this._dom_input.value.length === 0)
|
||||
style.fill = this._placeholderColor;
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
_deriveSurrogatePadding() {
|
||||
let indent = this._input_style.textIndent
|
||||
? parseFloat(this._input_style.textIndent)
|
||||
: 0;
|
||||
|
||||
if (this._input_style.padding && this._input_style.padding.length > 0) {
|
||||
let components = this._input_style.padding.trim().split(" ");
|
||||
|
||||
if (components.length == 1) {
|
||||
let padding = parseFloat(components[0]);
|
||||
return [padding, padding, padding, padding + indent];
|
||||
} else if (components.length == 2) {
|
||||
let paddingV = parseFloat(components[0]);
|
||||
let paddingH = parseFloat(components[1]);
|
||||
return [paddingV, paddingH, paddingV, paddingH + indent];
|
||||
} else if (components.length == 4) {
|
||||
let padding = components.map((component) => {
|
||||
return parseFloat(component);
|
||||
});
|
||||
padding[3] += indent;
|
||||
return padding;
|
||||
}
|
||||
}
|
||||
|
||||
return [0, 0, 0, indent];
|
||||
}
|
||||
|
||||
_deriveSurrogateText() {
|
||||
if (this._dom_input.value.length === 0) return this._placeholder;
|
||||
|
||||
if (this._dom_input.type == "password")
|
||||
return "•".repeat(this._dom_input.value.length);
|
||||
|
||||
return this._dom_input.value;
|
||||
}
|
||||
|
||||
_updateFontMetrics() {
|
||||
const style = this._deriveSurrogateStyle();
|
||||
// const font = style.toFontString()
|
||||
|
||||
// this._font_metrics = TextMetrics.measureFont(font)
|
||||
}
|
||||
|
||||
// CACHING OF INPUT BOX GRAPHICS
|
||||
|
||||
_buildBoxCache() {
|
||||
this._destroyBoxCache();
|
||||
|
||||
let states = ["DEFAULT", "FOCUSED", "DISABLED"];
|
||||
let input_bounds = this._getDOMInputBounds();
|
||||
|
||||
for (let state of states) {
|
||||
this._box_cache[state] = this._box_generator(
|
||||
input_bounds.width,
|
||||
input_bounds.height,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
this._previous.input_bounds = input_bounds;
|
||||
}
|
||||
|
||||
_destroyBoxCache() {
|
||||
if (this._box) {
|
||||
this.removeChild(this._box);
|
||||
this._box = null;
|
||||
}
|
||||
|
||||
for (let i in this._box_cache) {
|
||||
this._box_cache[i].destroy();
|
||||
this._box_cache[i] = null;
|
||||
delete this._box_cache[i];
|
||||
}
|
||||
}
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
_hasFocus() {
|
||||
return document.activeElement === this._dom_input;
|
||||
}
|
||||
|
||||
_setDOMInputVisible(visible) {
|
||||
this._dom_input.style.display = visible ? "block" : "none";
|
||||
}
|
||||
|
||||
_getCanvasBounds() {
|
||||
console.log("getting bounds");
|
||||
// let rect = this._last_renderer.view.getBoundingClientRect();
|
||||
let rect = this._last_renderer.view.screen;
|
||||
console.log(`Bounds: ${JSON.stringify(rect)}`);
|
||||
let bounds = {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
bounds.left += window.scrollX;
|
||||
bounds.top += window.scrollY;
|
||||
return bounds;
|
||||
}
|
||||
|
||||
_getDOMInputBounds() {
|
||||
let remove_after = false;
|
||||
|
||||
if (!this._dom_added) {
|
||||
// document.body.appendChild(this._dom_input);
|
||||
this.mountEl.appendChild(this._dom_input);
|
||||
remove_after = true;
|
||||
}
|
||||
|
||||
let org_transform = this._dom_input.style.transform;
|
||||
let org_display = this._dom_input.style.display;
|
||||
this._dom_input.style.transform = "";
|
||||
this._dom_input.style.display = "block";
|
||||
let bounds = this._dom_input.getBoundingClientRect();
|
||||
this._dom_input.style.transform = org_transform;
|
||||
this._dom_input.style.display = org_display;
|
||||
|
||||
if (remove_after) {
|
||||
this.mountEl.removeChild(this._dom_input);
|
||||
// document.body.removeChild(this._dom_input);
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
_getDOMRelativeWorldTransform() {
|
||||
// let canvas_bounds = this._last_renderer.view.getBoundingClientRect();
|
||||
let canvas_bounds = this._last_renderer.view.screen;
|
||||
let matrix = this.worldTransform.clone();
|
||||
|
||||
console.log(JSON.stringify(matrix));
|
||||
matrix.scale(this._resolution, this._resolution);
|
||||
matrix.scale(
|
||||
canvas_bounds.width / this._last_renderer.width,
|
||||
canvas_bounds.height / this._last_renderer.height,
|
||||
);
|
||||
return matrix;
|
||||
}
|
||||
|
||||
_pixiMatrixToCSS(m) {
|
||||
return "matrix(" + [m.a, m.b, m.c, m.d, m.tx, m.ty].join(",") + ")";
|
||||
}
|
||||
|
||||
_comparePixiMatrices(m1, m2) {
|
||||
if (!m1 || !m2) return false;
|
||||
return (
|
||||
m1.a == m2.a &&
|
||||
m1.b == m2.b &&
|
||||
m1.c == m2.c &&
|
||||
m1.d == m2.d &&
|
||||
m1.tx == m2.tx &&
|
||||
m1.ty == m2.ty
|
||||
);
|
||||
}
|
||||
|
||||
_compareClientRects(r1, r2) {
|
||||
if (!r1 || !r2) return false;
|
||||
return (
|
||||
r1.left == r2.left &&
|
||||
r1.top == r2.top &&
|
||||
r1.width == r2.width &&
|
||||
r1.height == r2.height
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function DefaultBoxGenerator(styles) {
|
||||
styles = styles || { fill: 0xcccccc };
|
||||
|
||||
if (styles.default) {
|
||||
styles.focused = styles.focused || styles.default;
|
||||
styles.disabled = styles.disabled || styles.default;
|
||||
} else {
|
||||
let temp_styles = styles;
|
||||
styles = {};
|
||||
styles.default = styles.focused = styles.disabled = temp_styles;
|
||||
}
|
||||
|
||||
return function (w, h, state) {
|
||||
let style = styles[state.toLowerCase()];
|
||||
let box = new Graphics({
|
||||
// roundPixels: true,
|
||||
// resolution: 8,
|
||||
});
|
||||
|
||||
if (style.fill) box.beginFill(style.fill);
|
||||
|
||||
if (style.stroke)
|
||||
box.lineStyle(
|
||||
style.stroke.width || 1,
|
||||
style.stroke.color || 0,
|
||||
style.stroke.alpha || 1,
|
||||
);
|
||||
|
||||
if (style.rounded) box.drawRoundedRect(0, 0, w, h, style.rounded);
|
||||
else box.drawRect(0, 0, w, h);
|
||||
|
||||
box.endFill();
|
||||
box.closePath();
|
||||
|
||||
return box;
|
||||
};
|
||||
}
|
||||
0
src/logo.ts
Normal file
0
src/logo.ts
Normal file
81
src/main.ts
Normal file
81
src/main.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { addIcon, Plugin } from "obsidian";
|
||||
import { TaskmapView } from "./TaskmapView";
|
||||
import { type PluginSettings, DEFAULT_SETTINGS } from "./PluginSettings";
|
||||
import { TaskmapSettingTab } from "./TaskmapSettingTab";
|
||||
import { DEFAULT_DATA } from "./ProjectData.svelte";
|
||||
import { LOGO_CONTENT, LOGO_NAME } from "./pixi/IconService";
|
||||
|
||||
export const FILE_EXTENSION = "taskmap";
|
||||
export const VIEW_TYPE_EXAMPLE = "example";
|
||||
|
||||
export default class TaskmapPlugin extends Plugin {
|
||||
static instance: TaskmapPlugin;
|
||||
settings: PluginSettings;
|
||||
|
||||
async onload() {
|
||||
TaskmapPlugin.instance = this;
|
||||
await this.loadSettings();
|
||||
this.registerView(VIEW_TYPE_EXAMPLE, (leaf) => new TaskmapView(leaf));
|
||||
this.registerExtensions([FILE_EXTENSION], VIEW_TYPE_EXAMPLE);
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
addIcon(LOGO_NAME, LOGO_CONTENT);
|
||||
this.addRibbonIcon(LOGO_NAME, "Taskmap Plugin", (_evt: MouseEvent) => {
|
||||
this.createAndOpenDrawing();
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new TaskmapSettingTab(this.app, this));
|
||||
|
||||
// // If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// // Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
// this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
// console.log('click', evt);
|
||||
// });
|
||||
|
||||
// // When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
// this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
public static getActiveView() {
|
||||
return TaskmapPlugin.instance.app.workspace.getActiveViewOfType(
|
||||
TaskmapView,
|
||||
);
|
||||
}
|
||||
|
||||
public async createAndOpenDrawing(): Promise<string> {
|
||||
this.app.workspace.detachLeavesOfType(VIEW_TYPE_EXAMPLE);
|
||||
|
||||
const file = await this.app.vault.create(
|
||||
`Example ${window.moment().format("YY-MM-DD hh.mm.ss")}.${FILE_EXTENSION}`,
|
||||
DEFAULT_DATA,
|
||||
);
|
||||
|
||||
const leaf = this.app.workspace.getLeaf("tab");
|
||||
|
||||
await leaf.openFile(file, { active: true });
|
||||
|
||||
leaf.setViewState({
|
||||
type: VIEW_TYPE_EXAMPLE,
|
||||
state: leaf.view.getState(),
|
||||
});
|
||||
|
||||
this.app.workspace.revealLeaf(
|
||||
this.app.workspace.getLeavesOfType(VIEW_TYPE_EXAMPLE)[0],
|
||||
);
|
||||
|
||||
return file.path;
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData(),
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
16
src/pixi/Constants.ts
Normal file
16
src/pixi/Constants.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export const BUTTON_SIZE = 18 * 2;
|
||||
export const TOOLBAR_GAP = 2;
|
||||
export const TOOLBAR_PADDING = { x: 4, y: 4 };
|
||||
export const TOOLBAR_SHIFT = 4 * 2;
|
||||
export const SUBTOOLBAR_SHIFT = 2;
|
||||
|
||||
export const TOOLBAR_SIZE = {
|
||||
width: 98 * 2,
|
||||
height: 22 * 2,
|
||||
};
|
||||
export const TASK_SIZE = {
|
||||
width: 280,
|
||||
height: 80,
|
||||
width_hovered: 284,
|
||||
height_hovered: 84,
|
||||
};
|
||||
76
src/pixi/Custom.js
Normal file
76
src/pixi/Custom.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { match } from "ts-pattern";
|
||||
|
||||
/**
|
||||
* Slides an element in and out.
|
||||
*
|
||||
* @param {Element} node
|
||||
* @param {SlideParamsCustom} [params]
|
||||
* @returns {TransitionConfig}
|
||||
*/
|
||||
export function slideCustom(
|
||||
node,
|
||||
{ delay = 0, duration = 400, easing = "cubic_out", axis = "y" } = {},
|
||||
) {
|
||||
const style = getComputedStyle(node);
|
||||
const opacity = +style.opacity;
|
||||
const primary_property = axis === "y" || axis === "-y" ? "height" : "width";
|
||||
const primary_property_value = parseFloat(style[primary_property]);
|
||||
const secondary_properties = match(axis)
|
||||
.with("y", () => ["top", "bottom"])
|
||||
.with("-y", () => ["top", "bottom"])
|
||||
.with("x", () => ["left", "right"])
|
||||
.with("-x", () => ["left", "right"])
|
||||
.otherwise((x) => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
const capitalized_secondary_properties = secondary_properties.map(
|
||||
(e) =>
|
||||
/** @type {'Left' | 'Right' | 'Top' | 'Bottom'} */ `${e[0].toUpperCase()}${e.slice(1)}`,
|
||||
);
|
||||
const padding_start_value = parseFloat(
|
||||
style[`padding${capitalized_secondary_properties[0]}`],
|
||||
);
|
||||
const padding_end_value = parseFloat(
|
||||
style[`padding${capitalized_secondary_properties[1]}`],
|
||||
);
|
||||
const margin_start_value = parseFloat(
|
||||
style[`margin${capitalized_secondary_properties[0]}`],
|
||||
);
|
||||
const margin_end_value = parseFloat(
|
||||
style[`margin${capitalized_secondary_properties[1]}`],
|
||||
);
|
||||
const border_width_start_value = parseFloat(
|
||||
style[`border${capitalized_secondary_properties[0]}Width`],
|
||||
);
|
||||
const border_width_end_value = parseFloat(
|
||||
style[`border${capitalized_secondary_properties[1]}Width`],
|
||||
);
|
||||
|
||||
let additionalString = "";
|
||||
if (axis === "-y") {
|
||||
}
|
||||
|
||||
return {
|
||||
delay,
|
||||
duration,
|
||||
easing,
|
||||
css: (t) =>
|
||||
"overflow: hidden;" +
|
||||
`opacity: ${Math.min(t * 20, 1) * opacity};` +
|
||||
`${primary_property}: ${t * primary_property_value}px;` +
|
||||
`padding-${secondary_properties[0]}: ${t * padding_start_value}px;` +
|
||||
`padding-${secondary_properties[1]}: ${t * padding_end_value}px;` +
|
||||
`margin-${secondary_properties[0]}: ${t * margin_start_value}px;` +
|
||||
`margin-${secondary_properties[1]}: ${t * margin_end_value}px;` +
|
||||
`border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
|
||||
`border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;` +
|
||||
`min-${primary_property}: 0` +
|
||||
(axis === "-y"
|
||||
? `; top: ${parseFloat(style["top"]) + primary_property_value * (1 - t)}px`
|
||||
: "") +
|
||||
(axis === "-x"
|
||||
? `; left: ${parseFloat(style["left"]) + primary_property_value * (1 - t)}px`
|
||||
: ""),
|
||||
};
|
||||
}
|
||||
137
src/pixi/GlobalState.svelte.ts
Normal file
137
src/pixi/GlobalState.svelte.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import TaskmapPlugin from "../main";
|
||||
import type { ProjectData } from "../ProjectData.svelte";
|
||||
import { StatusCode } from "../types";
|
||||
import { Spring } from "svelte/motion";
|
||||
import type { App } from "obsidian";
|
||||
import type { TaskmapView } from "../TaskmapView";
|
||||
|
||||
export class UIState {
|
||||
app: App;
|
||||
pressedButtonIndex = $state(-1);
|
||||
selectedTaskId = $state(-1);
|
||||
toolbarStatus = $state(StatusCode.DRAFT);
|
||||
projectData: ProjectData;
|
||||
taskPositions: Array<{
|
||||
taskId: number;
|
||||
tween: Spring<{ x: number; y: number }>;
|
||||
}>;
|
||||
// svg elements are pixelated zooming from scale < 1 to scale > 1, so we force a redraw manually
|
||||
updateOnZoomCounter = $state(0);
|
||||
|
||||
// parameters for animating task movement
|
||||
private tweenOptions = { duration: 300 };
|
||||
private springOptions = { stiffness: 0.07, damping: 0.7 };
|
||||
|
||||
constructor(projectData: ProjectData, app: App) {
|
||||
this.app = app;
|
||||
this.projectData = projectData;
|
||||
const posArray = projectData.tasks
|
||||
.filter((t) => !t.deleted)
|
||||
.map((task) => {
|
||||
const t = new Spring(
|
||||
this.calcTaskPosition(task.taskId),
|
||||
this.springOptions,
|
||||
);
|
||||
return { taskId: task.taskId, tween: t };
|
||||
});
|
||||
this.taskPositions = posArray;
|
||||
}
|
||||
|
||||
public incrementUpdateOnZoomCounter(): void {
|
||||
this.updateOnZoomCounter += 1;
|
||||
}
|
||||
|
||||
public updateTaskPositions() {
|
||||
this.taskPositions.forEach((taskPos) => {
|
||||
taskPos.tween.target = this.calcTaskPosition(taskPos.taskId);
|
||||
});
|
||||
}
|
||||
|
||||
public isSelected(taskId: number) {
|
||||
return this.selectedTaskId == taskId;
|
||||
}
|
||||
|
||||
public setSelectedTaskId(taskId: number) {
|
||||
this.selectedTaskId = taskId;
|
||||
if (taskId != -1) {
|
||||
this.toolbarStatus = this.projectData.getTaskStatus(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
// can be called from any component
|
||||
public save() {
|
||||
const x = TaskmapPlugin.getActiveView();
|
||||
if (x != null) {
|
||||
x.debouncedSave();
|
||||
console.log("saved");
|
||||
}
|
||||
}
|
||||
|
||||
public getActiveView(): TaskmapView {
|
||||
const x = TaskmapPlugin.getActiveView();
|
||||
if (x) {
|
||||
return x;
|
||||
} else {
|
||||
throw new Error("Unable to get active view");
|
||||
}
|
||||
}
|
||||
|
||||
public addTask() {
|
||||
const id = this.projectData.addTask();
|
||||
this.taskPositions.push({
|
||||
taskId: id,
|
||||
tween: new Spring(this.calcTaskPosition(id), this.springOptions),
|
||||
});
|
||||
this.save();
|
||||
}
|
||||
|
||||
public removeTask(id: number) {
|
||||
this.setSelectedTaskId(-1);
|
||||
this.projectData.removeTask(id);
|
||||
this.updateTaskPositions();
|
||||
this.save();
|
||||
}
|
||||
|
||||
public changeStatus(status: StatusCode) {
|
||||
console.log(
|
||||
`changeStatus from ${this.toolbarStatus} to ${status} for task ${this.selectedTaskId}`,
|
||||
);
|
||||
this.projectData.setTaskStatus(this.selectedTaskId, status);
|
||||
this.toolbarStatus = status;
|
||||
const x = TaskmapPlugin.getActiveView();
|
||||
if (x) {
|
||||
x.debouncedSave();
|
||||
}
|
||||
// get taskdata
|
||||
// change its status
|
||||
}
|
||||
|
||||
private calcTaskPosition(taskId: number) {
|
||||
const base_position = { x: 200, y: 500 };
|
||||
const idx = this.projectData.tasks
|
||||
.filter((t) => !t.deleted)
|
||||
.findIndex((t) => t.taskId == taskId);
|
||||
return {
|
||||
x: base_position.x + 200 * idx,
|
||||
y: base_position.y + 200 * idx,
|
||||
};
|
||||
}
|
||||
|
||||
public getCurrentTaskPosition(taskId: number) {
|
||||
const pos = this.taskPositions.find((t) => t.taskId === taskId)!.tween
|
||||
.current;
|
||||
if (taskId == 5) {
|
||||
console.log(JSON.stringify(pos));
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
public serializeForDebugging() {
|
||||
return JSON.stringify({
|
||||
pressedButtonIndex: this.pressedButtonIndex,
|
||||
selectedTaskId: this.selectedTaskId,
|
||||
toolbarStatus: this.toolbarStatus,
|
||||
projectData: this.projectData,
|
||||
});
|
||||
}
|
||||
}
|
||||
117
src/pixi/IconService.ts
Normal file
117
src/pixi/IconService.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { IconCode, StatusCode } from "../types";
|
||||
|
||||
export const LOGO_NAME = "TaskmapLogo";
|
||||
export const LOGO_CONTENT = `
|
||||
<path d="M88.9366 24.8308V33.8571H55.7464V24.8308H88.9366ZM91.1514 22.5714V11.2857C91.1514 10.0391 90.1586 9.02637 88.9366 9.02637H55.7464C54.5244 9.02637 53.5316 10.0391 53.5316 11.2857V22.5714C53.5316 23.818 54.5244 24.8308 55.7464 24.8308V33.8571L54.612 33.802C49.4075 33.2615 45.267 29.0378 44.7371 23.7287L44.683 22.5714V11.2857C44.683 5.05279 49.6363 9.0886e-08 55.7464 0H88.9366C95.0467 0 100 5.05279 100 11.2857V22.5714C100 28.4138 95.6484 33.2227 90.071 33.802L88.9366 33.8571V24.8308C90.1586 24.8308 91.1514 23.818 91.1514 22.5714Z" fill="currentColor"/>
|
||||
<path d="M88.9366 69.9736V79H55.7464V69.9736H88.9366ZM91.1514 67.7143V56.4286C91.1514 55.182 90.1586 54.1692 88.9366 54.1692H55.7464C54.5244 54.1692 53.5316 55.182 53.5316 56.4286V67.7143C53.5316 68.9609 54.5244 69.9736 55.7464 69.9736V79L54.612 78.9449C49.4075 78.4043 45.267 74.1806 44.7371 68.8715L44.683 67.7143V56.4286C44.683 50.1956 49.6363 45.1429 55.7464 45.1429H88.9366C95.0467 45.1429 100 50.1956 100 56.4286V67.7143C100 73.5566 95.6484 78.3656 90.071 78.9449L88.9366 79V69.9736C90.1586 69.9736 91.1514 68.9609 91.1514 67.7143Z" fill="currentColor"/>
|
||||
<path d="M22.8099 52.5212V26.4781C22.8099 17.752 29.7487 10.6737 38.3029 10.6737H47.6701V19.7111H38.3029C34.6368 19.7111 31.6692 22.7384 31.6692 26.4781V52.5212C31.6692 56.261 34.6368 59.2993 38.3029 59.2993H47.6701V68.3256H38.3029C29.7487 68.3256 22.8099 61.2473 22.8099 52.5212Z" fill="currentColor"/>
|
||||
<path d="M27.2371 34.9806V44.018H0V34.9806H27.2371Z" fill="currentColor"/>
|
||||
`;
|
||||
|
||||
export default class IconService {
|
||||
static getIcon = getIcon;
|
||||
static getStatusIcon = getStatusIcon;
|
||||
}
|
||||
|
||||
export function getIcon(code: IconCode) {
|
||||
switch (code) {
|
||||
case IconCode.TRASH:
|
||||
return LUCIDE_TRASH_SVG;
|
||||
case IconCode.TRASH_SINGLE:
|
||||
return LUCIDE_TRASH_SVG;
|
||||
case IconCode.TRASH_MULTIPLE:
|
||||
return LUCIDE_TRASH_SVG;
|
||||
case IconCode.KEY:
|
||||
return LUCIDE_KEY_SVG;
|
||||
case IconCode.LOCK:
|
||||
return LUCIDE_LOCK_SVG;
|
||||
case IconCode.FOCUS:
|
||||
return LUCIDE_FOCUS_SVG;
|
||||
case IconCode.STATUS:
|
||||
return getStatusIcon(StatusCode.DRAFT);
|
||||
case IconCode.STATUS_DRAFT:
|
||||
return getStatusIcon(StatusCode.DRAFT);
|
||||
case IconCode.STATUS_READY:
|
||||
return getStatusIcon(StatusCode.READY);
|
||||
case IconCode.STATUS_IN_PROGRESS:
|
||||
return getStatusIcon(StatusCode.IN_PROGRESS);
|
||||
case IconCode.STATUS_DONE:
|
||||
return getStatusIcon(StatusCode.DONE);
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusClassString(code: StatusCode) {
|
||||
return ["class=draft", "class=ready", "class=in-progress", "class=done"][
|
||||
code
|
||||
];
|
||||
}
|
||||
|
||||
export function getStatusIcon(code: StatusCode) {
|
||||
return `
|
||||
<svg
|
||||
class:is-pressed-up={isPressed}
|
||||
class:is-pressed-down={isPressedDown}
|
||||
${getStatusClassString(code)}
|
||||
class="circle" viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
const LUCIDE_TRASH_SVG = `
|
||||
<svg
|
||||
class="lucide lucide-trash2-icon lucide-trash-2"
|
||||
class:is-pressed-up={isPressed}
|
||||
class:is-pressed-down={isPressedDown}
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M10 11v6"/>
|
||||
<path d="M14 11v6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
`;
|
||||
const LUCIDE_KEY_SVG = `
|
||||
<svg
|
||||
class="lucide lucide-key-round-icon lucide-key-round"
|
||||
class:is-pressed-up={isPressed}
|
||||
class:is-pressed-down={isPressedDown}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/>
|
||||
<circle cx="16.5" cy="7.5" r=".5"/>
|
||||
</svg>
|
||||
`;
|
||||
const LUCIDE_LOCK_SVG = `
|
||||
<svg
|
||||
class:is-pressed-up={isPressed}
|
||||
class:is-pressed-down={isPressedDown}
|
||||
viewBox="0 0 24 24">
|
||||
<rect x="5" y="10" width="14" height="11" rx="2"/>
|
||||
<path d="M8 10V7a4 4 0 1 1 8 0v3"/>
|
||||
</svg>
|
||||
`;
|
||||
const LUCIDE_FOCUS_SVG = `
|
||||
<svg
|
||||
|
||||
class:is-pressed-up={isPressed}
|
||||
class:is-pressed-down={isPressedDown}
|
||||
|
||||
viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="8"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
|
||||
</svg>
|
||||
`;
|
||||
const LUCIDE_CIRCLE_SVG = `
|
||||
<svg
|
||||
|
||||
class:is-pressed-up={isPressed}
|
||||
class:is-pressed-down={isPressedDown}
|
||||
class="circle" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="9"/>
|
||||
</svg>
|
||||
`;
|
||||
43
src/types.ts
Normal file
43
src/types.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { ProjectData } from "./ProjectData.svelte";
|
||||
import { UIState } from "./pixi/GlobalState.svelte";
|
||||
import type { EasingFunction } from "svelte/transition";
|
||||
|
||||
export enum IconCode {
|
||||
TRASH,
|
||||
KEY,
|
||||
LOCK,
|
||||
FOCUS,
|
||||
STATUS,
|
||||
TRASH_SINGLE,
|
||||
TRASH_MULTIPLE,
|
||||
STATUS_DRAFT,
|
||||
STATUS_READY,
|
||||
STATUS_IN_PROGRESS,
|
||||
STATUS_DONE,
|
||||
}
|
||||
|
||||
export enum StatusCode {
|
||||
DRAFT,
|
||||
READY,
|
||||
IN_PROGRESS,
|
||||
DONE,
|
||||
}
|
||||
|
||||
export type TaskData = {
|
||||
name: string;
|
||||
taskId: number;
|
||||
status: StatusCode;
|
||||
deleted: boolean;
|
||||
};
|
||||
|
||||
export type Context = {
|
||||
uiState: UIState;
|
||||
projectData: ProjectData;
|
||||
};
|
||||
|
||||
export interface SlideParamsCustom {
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
easing?: EasingFunction;
|
||||
axis?: "x" | "y" | "-x" | "-y";
|
||||
}
|
||||
Loading…
Reference in a new issue