├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierrc ├── eslint.config.mjs ├── license.txt ├── locales ├── index.js ├── locales │ ├── cn.js │ └── en.js └── package.json ├── package.json ├── provider ├── .eslintrc.cjs ├── .gitignore ├── package.json ├── src │ ├── RestDataProvider.ts │ └── index.ts ├── test │ └── provider.spec.ts ├── tsconfig.json └── vite.config.js ├── readme.md ├── site ├── .gitignore ├── index.html ├── index.js ├── package.json ├── public │ └── placeholder.md ├── src │ ├── Component.svelte │ ├── Demo.svelte │ ├── Main.svelte │ ├── MainDemo.svelte │ ├── ThemeSelect.svelte │ ├── Toolbar.svelte │ ├── components │ │ ├── Component.svelte │ │ └── TooltipContent.svelte │ ├── customGantt │ │ └── TooltipContent.svelte │ ├── data.js │ └── data │ │ └── index.js ├── svelte.config.js └── vite.config.js ├── store ├── .eslintrc.cjs ├── .gitignore ├── license.txt ├── package.json ├── src │ ├── DataStore.ts │ ├── GanttDataTree.ts │ ├── columns.ts │ ├── dom │ │ └── scales.ts │ ├── helpers │ │ ├── actionHandlers.ts │ │ ├── menuOptions.ts │ │ ├── sort.ts │ │ └── toolbarButtons.ts │ ├── index.ts │ ├── links.ts │ ├── package.ts │ ├── scales.ts │ ├── sidebar.ts │ ├── tasks.ts │ ├── time.ts │ └── types.ts ├── test │ ├── columns.spec.ts │ ├── datastore.spec.ts │ ├── links.spec.ts │ ├── scales.spec.ts │ ├── sidebar.spec.ts │ ├── stubs │ │ ├── data.ts │ │ └── writable.ts │ ├── summaries.spec.ts │ ├── tasks.spec.ts │ └── time.spec.ts ├── tsconfig.json └── vite.config.js ├── svelte ├── .gitignore ├── cypress.config.js ├── cypress │ ├── e2e │ │ ├── basic │ │ │ ├── chart.cy.js │ │ │ ├── grid.cy.js │ │ │ ├── menus.cy.js │ │ │ ├── scales.cy.js │ │ │ ├── selection.cy.js │ │ │ └── toolbar.cy.js │ │ └── demos.cy.js │ ├── fixtures │ │ └── example.json │ └── support │ │ ├── commands.js │ │ └── e2e.js ├── demos │ ├── cases │ │ ├── BasicInit.svelte │ │ ├── ChartBorders.svelte │ │ ├── ContextMenu.svelte │ │ ├── ContextMenuHandler.svelte │ │ ├── ContextMenuOptions.svelte │ │ ├── DropDownMenu.svelte │ │ ├── GanttBackend.svelte │ │ ├── GanttBaseline.svelte │ │ ├── GanttBatchProvider.svelte │ │ ├── GanttCustomSort.svelte │ │ ├── GanttCustomZoom.svelte │ │ ├── GanttFixedColumns.svelte │ │ ├── GanttFlexColumns.svelte │ │ ├── GanttForm.svelte │ │ ├── GanttFormControls.svelte │ │ ├── GanttFullscreen.svelte │ │ ├── GanttGrid.svelte │ │ ├── GanttHolidays.svelte │ │ ├── GanttLengthUnit.svelte │ │ ├── GanttLocale.svelte │ │ ├── GanttMarkers.svelte │ │ ├── GanttMultiple.svelte │ │ ├── GanttNoGrid.svelte │ │ ├── GanttPerformance.svelte │ │ ├── GanttPreventActions.svelte │ │ ├── GanttProvider.svelte │ │ ├── GanttReadOnly.svelte │ │ ├── GanttScales.svelte │ │ ├── GanttSizes.svelte │ │ ├── GanttSort.svelte │ │ ├── GanttStartEnd.svelte │ │ ├── GanttSummariesConvert.svelte │ │ ├── GanttSummariesNoDrag.svelte │ │ ├── GanttSummariesProgress.svelte │ │ ├── GanttTaskTypes.svelte │ │ ├── GanttText.svelte │ │ ├── GanttToolbar.svelte │ │ ├── GanttToolbarButtons.svelte │ │ ├── GanttToolbarCustom.svelte │ │ ├── GanttTooltips.svelte │ │ └── GanttZoom.svelte │ ├── common │ │ ├── Index.svelte │ │ ├── Link.svelte │ │ ├── ListRoutes.svelte │ │ ├── Router.svelte │ │ └── helpers.js │ ├── custom │ │ ├── AvatarCell.svelte │ │ ├── Form.svelte │ │ ├── MyTaskContent.svelte │ │ ├── MyTooltipContent.svelte │ │ └── placeholder.md │ ├── data.js │ ├── index.js │ ├── routes.js │ └── skins.js ├── index.html ├── license.txt ├── package.json ├── postcss.config.js ├── readme.md ├── src │ ├── components │ │ ├── ContextMenu.svelte │ │ ├── Fullscreen.svelte │ │ ├── Gantt.svelte │ │ ├── Layout.svelte │ │ ├── Resizer.svelte │ │ ├── TimeScale.svelte │ │ ├── Toolbar.svelte │ │ ├── chart │ │ │ ├── Bars.svelte │ │ │ ├── CellGrid.svelte │ │ │ ├── Chart.svelte │ │ │ └── Links.svelte │ │ ├── grid │ │ │ ├── ActionCell.svelte │ │ │ ├── Grid.svelte │ │ │ └── TextCell.svelte │ │ └── sidebar │ │ │ ├── Links.svelte │ │ │ └── SideBar.svelte │ ├── helpers │ │ ├── hotkey.js │ │ ├── locate.js │ │ └── reorder.js │ ├── index.js │ ├── themes │ │ ├── Material.svelte │ │ ├── Willow.svelte │ │ └── WillowDark.svelte │ └── widgets │ │ ├── Counter.svelte │ │ ├── IconButton.svelte │ │ └── Tooltip.svelte ├── svelte.config.js ├── tests │ ├── Index.svelte │ ├── cases │ │ └── LocalData.svelte │ ├── common │ │ ├── Index.svelte │ │ └── ListRoutes.svelte │ ├── data.js │ ├── index.html │ ├── index.js │ └── routes.js ├── vite.config.js └── whatsnew.md ├── vitest.workspace.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true, 7 | }, 8 | extends: ["plugin:cypress/recommended", "eslint:recommended", "prettier"], 9 | parserOptions: { 10 | ecmaVersion: 2020, 11 | sourceType: "module", 12 | extraFileExtensions: [".svelte"], 13 | }, 14 | plugins: ["svelte3"], 15 | 16 | overrides: [ 17 | { 18 | files: ["*.svelte"], 19 | processor: "svelte3/svelte3", 20 | }, 21 | ], 22 | settings: { 23 | // [todo] we can add stylelint for this 24 | "svelte3/ignore-styles": () => true, 25 | }, 26 | rules: { 27 | "cypress/no-unnecessary-waiting": 0, 28 | "cypress/no-assigning-return-values": 0, 29 | "no-bitwise": ["error"], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | 4 | *.zip 5 | .Ds_store 6 | *.tgz 7 | *.log 8 | .vscode 9 | .idea 10 | .env.local -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yarn run lint-staged 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "semi": true, 4 | "singleQuote": false, 5 | "quoteProps": "as-needed", 6 | "trailingComma": "es5", 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "svelteSortOrder": "options-scripts-markup-styles", 10 | "plugins": [ 11 | "prettier-plugin-svelte" 12 | ], 13 | "overrides": [ 14 | { 15 | "files": "*.svelte", 16 | "options": { 17 | "parser": "svelte" 18 | } 19 | }, 20 | { 21 | "files": "*.ts", 22 | "options": { 23 | "parser": "typescript" 24 | } 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintConfigPrettier from "eslint-config-prettier"; 2 | import eslintPluginSvelte from 'eslint-plugin-svelte'; 3 | import tsLint from "typescript-eslint"; 4 | import jsLint from "@eslint/js"; 5 | import vitest from "eslint-plugin-vitest"; 6 | import globals from "globals"; 7 | 8 | export default [{ 9 | ignores: ["node_modules/", "dist/", "build/", "coverage/", "public/", "svelte/vite.config.js"], 10 | }, 11 | jsLint.configs.recommended, 12 | ...tsLint.configs.recommended, 13 | ...eslintPluginSvelte.configs['flat/recommended'], 14 | eslintConfigPrettier, 15 | vitest.configs.recommended, 16 | ...eslintPluginSvelte.configs["flat/prettier"], 17 | { 18 | rules: { 19 | "no-bitwise": ["error"], 20 | // there is a misconception between esLint and svelte compiler 21 | // rules that are necessary for compiler, throw errors in esLint 22 | // need to be revised with next version of toolchain 23 | "svelte/no-unused-svelte-ignore": "off", 24 | "svelte/valid-compile": "off", 25 | // Ignore unused vars starting with _ 26 | // "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 27 | // // Turn off the need for explicit function return types 28 | // "@typescript-eslint/explicit-function-return-type": "off", 29 | // // Warn when "any" type is used 30 | "@typescript-eslint/no-explicit-any": "off", 31 | // // Warn on @ts-ignore comments 32 | // "@typescript-eslint/ban-ts-comment": "warn", 33 | // // Public methods should have return types 34 | // "@typescript-eslint/explicit-module-boundary-types": "error", 35 | }, 36 | }, 37 | { 38 | languageOptions: { 39 | globals: { ...globals.browser, ...globals.es2022 }, 40 | ecmaVersion: 2022, 41 | sourceType: "module", 42 | parserOptions: { 43 | extraFileExtensions: [".svelte"], 44 | warnOnUnsupportedTypeScriptVersion: false, 45 | tsconfigRootDir: import.meta.dirname, 46 | }, 47 | }, 48 | 49 | }, 50 | { 51 | 52 | files: ["**/*.svelte"], 53 | rules: { 54 | "@typescript-eslint/no-unused-expressions": "off" 55 | } 56 | }, 57 | { 58 | // temporarily ignore test folders 59 | ignores: [ 60 | "**/cypress/", "**/test/" 61 | ] 62 | } 63 | ]; 64 | -------------------------------------------------------------------------------- /locales/index.js: -------------------------------------------------------------------------------- 1 | export { default as en } from "./locales/en.js"; 2 | export { default as cn } from "./locales/cn.js"; 3 | -------------------------------------------------------------------------------- /locales/locales/cn.js: -------------------------------------------------------------------------------- 1 | export default { 2 | gantt: { 3 | // Header / sidebar 4 | "Task name": "任务名称", 5 | "Start date": "开始日期", 6 | Duration: "期间", 7 | Task: "任务", 8 | Milestone: "里程碑", 9 | "Summary task": "总结任务", 10 | 11 | // Sidebar 12 | Save: "保存", 13 | Delete: "删除", 14 | Name: "名称", 15 | Description: "描述", 16 | "Select type": "选择类型", 17 | Type: "类型", 18 | "End date": "结束日期", 19 | Progress: "进步", 20 | Predecessors: "前辈", 21 | Successors: "后继者", 22 | "Add task name": "添加任务名称", 23 | "Add description": "添加描述", 24 | "Select link type": "选择链接类型", 25 | "End-to-start": "结束开始", 26 | "Start-to-start": "开始开始", 27 | "End-to-end": "端到端", 28 | "Start-to-end": "开始到结束", 29 | 30 | // Context menu / toolbar 31 | Add: "添加", 32 | "Child task": "子任务", 33 | "Task above": "上面的任务", 34 | "Task below": "下面的任务", 35 | "Convert to": "转变", 36 | Edit: "编辑", 37 | Cut: "切", 38 | Copy: "复制", 39 | Paste: "粘贴", 40 | Move: "移动", 41 | Up: "向上", 42 | Down: "下", 43 | Indent: "缩进", 44 | Outdent: "凹痕", 45 | "Split task": "拆分任务", 46 | 47 | // Toolbar 48 | "New task": "新任务", 49 | "Move up": "提升", 50 | "Move down": "下移", 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /locales/locales/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | gantt: { 3 | // Header / sidebar 4 | "Task name": "Task name", 5 | "Start date": "Start date", 6 | Duration: "Duration", 7 | Task: "Task", 8 | Milestone: "Milestone", 9 | "Summary task": "Summary task", 10 | 11 | // Sidebar 12 | Save: "Save", 13 | Delete: "Delete", 14 | Name: "Name", 15 | Description: "Description", 16 | "Select type": "Select type", 17 | Type: "Type", 18 | "End date": "End date", 19 | Progress: "Progress", 20 | Predecessors: "Predecessors", 21 | Successors: "Successors", 22 | "Add task name": "Add task name", 23 | "Add description": "Add description", 24 | "Select link type": "Select link type", 25 | "End-to-start": "End-to-start", 26 | "Start-to-start": "Start-to-start", 27 | "End-to-end": "End-to-end", 28 | "Start-to-end": "Start-to-end", 29 | 30 | // Context menu / toolbar 31 | Add: "Add", 32 | "Child task": "Child task", 33 | "Task above": "Task above", 34 | "Task below": "Task below", 35 | "Convert to": "Convert to", 36 | Edit: "Edit", 37 | Cut: "Cut", 38 | Copy: "Copy", 39 | Paste: "Paste", 40 | Move: "Move", 41 | Up: "Up", 42 | Down: "Down", 43 | Indent: "Indent", 44 | Outdent: "Outdent", 45 | "Split task": "Split task", 46 | 47 | // Toolbar 48 | "New task": "New task", 49 | "Move up": "Move up", 50 | "Move down": "Move down", 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /locales/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wx-gantt-locales", 3 | "version": "2.1.1", 4 | "description": "Locales for WX Gantt widget", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "true", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [ 12 | "wx", 13 | "gantt", 14 | "locales" 15 | ], 16 | "author": "", 17 | "license": "MIT" 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "wx-gantt", 4 | "workspaces": [ 5 | "svelte", 6 | "store", 7 | "provider", 8 | "locales", 9 | "site" 10 | ], 11 | "scripts": { 12 | "build:deps": "run-s build:store build:provider", 13 | "build:provider": "cd provider && shx rm -f ./dist/index.js && yarn build", 14 | "build:site": "cd site && yarn build", 15 | "build:store": "cd store && shx rm -f ./dist/index.js && yarn build", 16 | "build:tests": "cd svelte && yarn build:tests", 17 | "build": "cd svelte && yarn build", 18 | "lint": "yarn eslint ./svelte/src ./svelte/demos ./store/src ./provider/src", 19 | "prepare": "husky", 20 | "start:demos": "cd svelte && yarn start", 21 | "start:site": "cd site && yarn start", 22 | "start:tests": "cd svelte && yarn start:tests", 23 | "start": "run-s build:deps start:demos", 24 | "test:cypress": "cd svelte && yarn test:cypress", 25 | "test": "vitest --run", 26 | "watch:deps": "run-p watch:store watch:provider", 27 | "watch:provider": "cd provider && shx rm -f ./dist/index.js && yarn watch", 28 | "watch:site": "run-p watch:deps start:site", 29 | "watch:store": "cd store && shx rm -f ./dist/index.js && yarn watch", 30 | "watch:tests": "run-p watch:deps start:tests", 31 | "watch": "run-p watch:deps start:demos" 32 | }, 33 | "devDependencies": { 34 | "@sveltejs/vite-plugin-svelte": "4.0.0", 35 | "@vitest/coverage-v8": "1.6.0", 36 | "wx-vite-tools": "1.0.5", 37 | "autoprefixer": "10.4.20", 38 | "cypress": "13.6.4", 39 | "eslint": "9.14.0", 40 | "eslint-config-prettier": "9.1.0", 41 | "eslint-plugin-cypress": "4.1.0", 42 | "eslint-plugin-svelte": "2.46.0", 43 | "eslint-plugin-vitest": "0.5.4", 44 | "husky": "9.1.6", 45 | "lint-staged": "15.2.10", 46 | "npm-run-all": "4.1.5", 47 | "postcss": "8.4.47", 48 | "prettier": "3.3.3", 49 | "prettier-plugin-svelte": "3.2.7", 50 | "rollup-plugin-visualizer": "5.12.0", 51 | "shx": "0.3.4", 52 | "svelte": "5.1.9", 53 | "svelte-spa-router": "4.0.1", 54 | "typescript-eslint": "8.13.0", 55 | "typescript": "5.6.3", 56 | "vite-plugin-conditional-compile": "1.4.5", 57 | "vite-plugin-dts": "3.7.2", 58 | "vite": "5.4.10", 59 | "vitest": "1.5.0" 60 | }, 61 | "lint-staged": { 62 | "*.{ts,js,svelte}": [ 63 | "eslint --fix --no-warn-ignored", 64 | "prettier --write" 65 | ], 66 | "*.{css,md,json}": [ 67 | "prettier --write" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /provider/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | env: { 5 | browser: true, 6 | node: true, 7 | es6: true, 8 | }, 9 | extends: [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier", 13 | ], 14 | parserOptions: { 15 | ecmaVersion: 2018, 16 | sourceType: "module", 17 | 18 | tsconfigRootDir: __dirname, 19 | }, 20 | plugins: ["@typescript-eslint"], 21 | rules: { 22 | "no-bitwise": ["error"], 23 | "@typescript-eslint/explicit-module-boundary-types": "off", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /provider/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /provider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wx-gantt-data-provider", 3 | "version": "2.1.1", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/types/index.d.ts", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "vite build", 11 | "watch": "vite build --mode development -w", 12 | "lint": "yarn eslint ./src", 13 | "test": "vitest --run", 14 | "coverage": "vitest --run --coverage" 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "dependencies": { 20 | "wx-lib-data-provider": "1.5.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /provider/src/index.ts: -------------------------------------------------------------------------------- 1 | import RestDataProvider from "./RestDataProvider"; 2 | 3 | export { RestDataProvider }; 4 | -------------------------------------------------------------------------------- /provider/test/provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { RestDataProvider } from "../src/index"; 3 | 4 | function getDataStore() { 5 | const provider = new RestDataProvider(""); 6 | return { provider }; 7 | } 8 | 9 | describe("data provider", () => { 10 | it("can be initialized", () => { 11 | const t = getDataStore(); 12 | expect(t).to.not.eq(null); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /provider/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "esnext", 5 | "lib": ["ESNext", "DOM"], 6 | "target": "ESNext", 7 | 8 | "isolatedModules": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "allowJs": true, 14 | "checkJs": false, 15 | 16 | "noImplicitAny": true, 17 | "sourceMap": true, 18 | 19 | "baseUrl": ".", 20 | "declaration": true, 21 | "outDir": "dist/" 22 | }, 23 | "watchOptions": {}, 24 | "include": ["src/**/*.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /provider/vite.config.js: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { resolve } from "path"; 3 | import { defineConfig, loadEnv } from "vite"; 4 | import dts from "vite-plugin-dts"; 5 | import { waitChanges, waitOn } from "wx-vite-tools"; 6 | 7 | export default async ({ mode }) => { 8 | process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; 9 | 10 | const files = 11 | mode === "production" 12 | ? [] 13 | : [resolve(__dirname, "../store/dist/index.js")]; 14 | 15 | const config = { 16 | build: { 17 | lib: { 18 | entry: resolve(__dirname, "src/index.ts"), 19 | name: "provider", 20 | formats: ["es"], 21 | fileName: () => `index.js`, 22 | }, 23 | sourcemap: true, 24 | minify: false, 25 | target: "esnext", 26 | }, 27 | test: { 28 | coverage: { 29 | reporter: ["text"], 30 | }, 31 | }, 32 | watch: { 33 | persistent: true, 34 | include: ["src/**/*.ts", "src/**/*.js"], 35 | }, 36 | plugins: [ 37 | waitChanges({ files }), 38 | dts({ outDir: resolve(__dirname, "dist/types") }), 39 | ], 40 | }; 41 | 42 | return waitOn({ files }).then(() => defineConfig(config)); 43 | }; 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # SVAR Svelte Gantt Chart 4 | 5 |
6 | 7 |
8 | 9 | :globe_with_meridians: [Website](https://svar.dev/svelte/gantt/) • :bulb: [Getting Started](https://docs.svar.dev/svelte/gantt/getting_started/) • :eyes: [Demos](https://docs.svar.dev/svelte/gantt/samples/#/base/willow) 10 | 11 |
12 | 13 |
14 | 15 | [![npm](https://img.shields.io/npm/v/wx-svelte-gantt.svg)](https://www.npmjs.com/package/wx-svelte-gantt) 16 | [![License](https://img.shields.io/github/license/svar-widgets/gantt)](https://github.com/svar-widgets/gantt/blob/main/license.txt) 17 | [![npm downloads](https://img.shields.io/npm/dm/wx-svelte-gantt.svg)](https://www.npmjs.com/package/wx-svelte-gantt) 18 | 19 |
20 | 21 | **SVAR Svelte Gantt** is a customizable, easy-to-use, and interactive Gantt chart component written in Svelte. Its intuitive interface allows users to add and manage tasks and dependencies directly on the timeline using drag-and-drop or via a simple task edit form. 22 | 23 |
24 | UI of SVAR Svelte Gantt Chart - Screenshot 25 |
26 | 27 | ### ✨ Key Features 28 | 29 | - Interactive drag-and-drop interface 30 | - Intuitive and customizable task edit form 31 | - Set task dependencies on the timeline or in a popup form 32 | - Showing task progress on the taskbar 33 | - Hierarchical view of sub tasks 34 | - Reordering tasks in grid with drag-and-drop 35 | - Configurable timeline (hours, days, weeks) 36 | - Ability to use custom HTML in grid cells 37 | - Toolbar and context menu 38 | - Tooltips for taskbars 39 | - Zooming with scroll 40 | - Fast performance with large data sets 41 | - Light and dark skins 42 | 43 | ### 🔧 Svelte 4 and Svelte 5 versions 44 | 45 | There are two versions of the library: the 1.x version – designed to work with Svelte 4, and the 2.x version – created for Svelte 5. 46 | 47 | To use the SVAR Gantt for Svelte 5, install it as follows: 48 | 49 | ``` 50 | npm install wx-svelte-gantt 51 | ``` 52 | 53 | To use the SVAR Gantt for Svelte 4: 54 | 55 | ``` 56 | npm install wx-svelte-gantt@1.2.0 57 | ``` 58 | 59 | ### 🛠️ How to Use 60 | 61 | To use the widget, simply import the package and include the component in your Svelte file: 62 | 63 | ```svelte 64 | 86 | 87 | 88 | ``` 89 | 90 | For further instructions, follow the detailed [how-to-start guide](https://docs.svar.dev/svelte/gantt/getting_started/). 91 | 92 | ### 💻 How to Modify 93 | 94 | Typically, you don't need to modify the code. However, if you wish to do so, follow these steps: 95 | 96 | 1. Run `yarn` to install dependencies. Note that this project is a monorepo using `yarn` workspaces, so npm will not work 97 | 2. Start the project in development mode with `yarn start` 98 | 99 | ### ✅ Run Tests 100 | 101 | To run the test: 102 | 103 | 1. Start the test examples with: 104 | ```sh 105 | yarn start:tests 106 | ``` 107 | 2. In a separate console, run the end-to-end tests with: 108 | ```sh 109 | yarn test:cypress 110 | ``` 111 | 112 | ### :speech_balloon: Need Help? 113 | 114 | [Post an Issue](https://github.com/svar-widgets/gantt/issues/) or use our [community forum](https://forum.svar.dev). 115 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Svelte Widgets 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /site/index.js: -------------------------------------------------------------------------------- 1 | import Demo from "./src/Demo.svelte"; 2 | import { mount } from "svelte"; 3 | 4 | mount(Demo, { 5 | target: document.querySelector("#wx_demo_area") || document.body, 6 | props: { 7 | themeSelect: false, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wx-site-svelte-gantt", 3 | "version": "2.1.1", 4 | "type": "module", 5 | "scripts": { 6 | "build": "vite build", 7 | "lint": "yarn eslint ./src", 8 | "start": "yarn vite --open" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "wx-svelte-core": "2.0.1", 13 | "wx-svelte-gantt": "2.1.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /site/public/placeholder.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svar-widgets/gantt/6cef8d80921105007ee4494f3c9a5e3d286cf049/site/public/placeholder.md -------------------------------------------------------------------------------- /site/src/Component.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 |
42 | 43 | 44 | 53 | 54 | 55 |
56 | 57 | 73 | -------------------------------------------------------------------------------- /site/src/Demo.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | {#if themeSelect} 25 |
26 |
27 |
28 | Theme 29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 | {:else} 37 |
38 | {/if} 39 | 40 | 41 |
42 | 43 | 76 | -------------------------------------------------------------------------------- /site/src/Main.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 |
13 | 14 |
15 |
16 | Theme 17 | 18 |
19 |
20 |
21 | {#key skin} 22 | 23 | {/key} 24 |
25 |
26 | 27 | 51 | -------------------------------------------------------------------------------- /site/src/MainDemo.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 |
17 | 18 |
19 |
20 | Theme 21 | 22 |
23 |
24 |
25 | {#key skin} 26 | 27 | {/key} 28 |
29 |
30 | 31 | 55 | -------------------------------------------------------------------------------- /site/src/ThemeSelect.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | 25 | {#snippet children(option)} 26 |
27 |
28 |
29 |
30 |
31 | {option.label} 32 |
33 | 51 | {/snippet} 52 |
53 |
54 | -------------------------------------------------------------------------------- /site/src/Toolbar.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /site/src/components/Component.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 |
42 | 43 | 44 | 53 | 54 | 55 |
56 | 57 | 73 | -------------------------------------------------------------------------------- /site/src/components/TooltipContent.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if data} 9 |
10 |
11 | {data.type}: 12 | {data.text} 13 |
14 |
15 | start: 16 | {format(data.start, mask)} 17 |
18 | {#if data.end} 19 |
20 | end: 21 | {format(data.end, mask)} 22 |
23 | {/if} 24 |
25 | {/if} 26 | 27 | 50 | -------------------------------------------------------------------------------- /site/src/customGantt/TooltipContent.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if data} 9 |
10 |
11 | {data.type}: 12 | {data.text} 13 |
14 |
15 | start: 16 | {format(data.start, mask)} 17 |
18 | {#if data.end} 19 |
20 | end: 21 | {format(data.end, mask)} 22 |
23 | {/if} 24 |
25 | {/if} 26 | 27 | 50 | -------------------------------------------------------------------------------- /site/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | }; 8 | -------------------------------------------------------------------------------- /site/vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | 4 | export default () => { 5 | let build, 6 | publicDir = resolve(__dirname, "public"), 7 | server = {}, 8 | base = "", 9 | plugins = [svelte({})]; 10 | 11 | build = { 12 | rollupOptions: { 13 | input: { index: resolve(__dirname, "index.html") }, 14 | }, 15 | }; 16 | 17 | return { 18 | base, 19 | build, 20 | publicDir, 21 | resolve: { dedupe: ["svelte"] }, 22 | plugins, 23 | server, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /store/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | env: { 5 | browser: true, 6 | node: true, 7 | es6: true, 8 | }, 9 | extends: [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier", 13 | ], 14 | parserOptions: { 15 | ecmaVersion: 2018, 16 | sourceType: "module", 17 | 18 | tsconfigRootDir: __dirname, 19 | }, 20 | plugins: ["@typescript-eslint"], 21 | rules: { 22 | "no-bitwise": ["error"], 23 | "@typescript-eslint/explicit-module-boundary-types": "off", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /store/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wx-gantt-store", 3 | "version": "2.1.1", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/types/index.d.ts", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "vite build", 11 | "watch": "vite build --mode development -w", 12 | "lint": "yarn eslint ./src", 13 | "test": "vitest --run", 14 | "coverage": "vitest --run --coverage" 15 | }, 16 | "files": [ 17 | "dist", 18 | "license.txt" 19 | ], 20 | "dependencies": { 21 | "wx-lib-state": "1.9.0", 22 | "date-fns": "3.6.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /store/src/columns.ts: -------------------------------------------------------------------------------- 1 | import type { GanttColumn } from "./types"; 2 | import { format } from "./time"; 3 | 4 | export function normalizeColumns(columns: GanttColumn[]): GanttColumn[] { 5 | if (!columns || !columns.length) { 6 | return []; 7 | } 8 | 9 | const addTaskColumn = columns.find(col => col.id === "action"); 10 | if (!addTaskColumn) { 11 | columns = [...columns, expandColumn]; 12 | } 13 | 14 | const resColumns = columns.map(a => { 15 | const align = a.align || "left"; 16 | const isActionColumn = a.id === "action"; 17 | const flexgrow = !isActionColumn && a.flexgrow ? a.flexgrow : null; 18 | const width = flexgrow ? 1 : a.width || (isActionColumn ? 50 : 120); 19 | 20 | let action; 21 | if (a.id === "action") action = addTaskColumn ? "add-task" : "expand"; 22 | 23 | let template = a.template; 24 | if (!template) { 25 | switch (a.id) { 26 | case "start": 27 | template = b => format(b, "dd-MM-yyyy"); 28 | break; 29 | case "end": 30 | template = b => format(b, "dd-MM-yyyy"); 31 | break; 32 | } 33 | } 34 | 35 | return { 36 | width, 37 | align, 38 | header: a.header, 39 | id: a.id, 40 | template, 41 | ...(flexgrow && { flexgrow }), 42 | ...(action && { action }), 43 | cell: a.cell, 44 | resize: a.resize ?? true, 45 | sort: a.sort ?? !action, 46 | }; 47 | }); 48 | 49 | return resColumns; 50 | } 51 | 52 | export const defaultColumns: GanttColumn[] = [ 53 | { id: "text", header: "Task name", flexgrow: 1, sort: true }, 54 | { id: "start", header: "Start date", align: "center", sort: true }, 55 | { 56 | id: "duration", 57 | header: "Duration", 58 | width: 100, 59 | align: "center", 60 | sort: true, 61 | }, 62 | { 63 | id: "action", 64 | header: "", 65 | width: 50, 66 | align: "center", 67 | sort: false, 68 | resize: false, 69 | }, 70 | ]; 71 | 72 | export const expandColumn: GanttColumn = { 73 | id: "action", 74 | header: "", 75 | align: "center", 76 | width: 50, 77 | sort: false, 78 | resize: false, 79 | }; 80 | -------------------------------------------------------------------------------- /store/src/dom/scales.ts: -------------------------------------------------------------------------------- 1 | 2 | export function grid( 3 | width: number, 4 | height: number, 5 | color: string, 6 | mode?: "full" 7 | ): string { 8 | // FIXME :: Svelte-kit 9 | if (typeof document === "undefined") return ""; 10 | const canvas = document.createElement("canvas"); 11 | 12 | let fillMode = true; 13 | 14 | if (fillMode) { 15 | const ctx = canvasSize(canvas, width, height, 1, color); 16 | renderCell(ctx, mode, 0, width, 0, height); 17 | } 18 | // #endif 19 | return canvas.toDataURL(); 20 | } 21 | 22 | function canvasSize( 23 | canvas: HTMLCanvasElement, 24 | width: number, 25 | height: number, 26 | zoom: number, 27 | color: string 28 | ): CanvasRenderingContext2D { 29 | canvas.setAttribute("width", (width * zoom).toString()); 30 | canvas.setAttribute("height", (height * zoom).toString()); 31 | const ctx = canvas.getContext("2d"); 32 | ctx.translate(-0.5, -0.5); 33 | ctx.strokeStyle = color; 34 | 35 | return ctx; 36 | } 37 | 38 | function renderCell( 39 | ctx: CanvasRenderingContext2D, 40 | mode: string, 41 | xA: number, 42 | xB: number, 43 | yA: number, 44 | yB: number 45 | ) { 46 | ctx.beginPath(); 47 | ctx.moveTo(xB, yA); 48 | ctx.lineTo(xB, yB); 49 | 50 | if (mode === "full") ctx.lineTo(xA, yB); 51 | ctx.stroke(); 52 | } 53 | -------------------------------------------------------------------------------- /store/src/helpers/actionHandlers.ts: -------------------------------------------------------------------------------- 1 | import type { TID } from "wx-lib-state"; 2 | import type { GanttDataTree } from "../types"; 3 | import { IOptionConfig } from "./menuOptions"; 4 | import { IButtonConfig } from "./toolbarButtons"; 5 | 6 | export function handleAction( 7 | api: any, 8 | action: string, 9 | target: TID | null, 10 | _: any 11 | ): void { 12 | const { selected, tasks } = api.getState(); 13 | const hasSelection = selected.length; 14 | const targetless = !hasSelection && action === "add-task"; 15 | // single-target ops 16 | const single = ["edit-task", "paste-task"]; 17 | // do not sort index-/level-wise 18 | const avoidMap = ["copy-task", "cut-task"]; 19 | // apply in reverse to maintain relative positions 20 | const reversed = [ 21 | "copy-task", 22 | "cut-task", 23 | "delete-task", 24 | "indent-task:remove", 25 | "move-task:down", 26 | ]; 27 | // direct children should move with entire branch 28 | const checkParent = ["indent-task:add", "move-task:down", "move-task:up"]; 29 | const checkLevel: { [k: string]: number } = { 30 | "indent-task:remove": 2, 31 | }; 32 | const limit = { 33 | parent: checkParent.includes(action), 34 | level: checkLevel[action], 35 | }; 36 | 37 | target = target || (hasSelection ? selected[selected.length - 1] : null); 38 | if (!target && !targetless) return; 39 | 40 | if (action !== "paste-task") api._temp = null; 41 | 42 | if (single.includes(action) || targetless || selected.length === 1) { 43 | runSingleAction(api, action, target, _); 44 | } else if (hasSelection) { 45 | const order = avoidMap.includes(action) 46 | ? selected 47 | : mapOrder(selected, tasks, limit); 48 | if (reversed.includes(action)) { 49 | order.reverse(); 50 | } 51 | order.forEach((id: TID) => runSingleAction(api, action, id, _)); 52 | } 53 | } 54 | 55 | function mapOrder( 56 | selected: TID[], 57 | tasks: GanttDataTree, 58 | limit: { parent: boolean; level: number } 59 | ): TID[] { 60 | let order = selected.map(id => { 61 | const tobj = tasks.byId(id); 62 | return { 63 | id, 64 | level: tobj.$level, 65 | parent: tobj.parent, 66 | index: tasks.getIndexById(id), 67 | }; 68 | }); 69 | if (limit.parent || limit.level) { 70 | order = order.filter(obj => { 71 | const ignoreParent = limit.level && obj.level <= limit.level; 72 | return ignoreParent || !selected.includes(obj.parent); 73 | }); 74 | } 75 | order.sort((a, b) => { 76 | return a.level - b.level || a.index - b.index; 77 | }); 78 | return order.map(o => o.id); 79 | } 80 | 81 | function runSingleAction(api: any, action: string, target: TID, _: any): void { 82 | let op: string = action.split(":")[0]; 83 | let mode: string | boolean = action.split(":")[1]; 84 | let data: any = { id: target }; 85 | let extraData: any = {}; 86 | 87 | if (op == "copy-task" || op == "cut-task") { 88 | if (!api._temp) api._temp = []; 89 | api._temp.push({ id: target, cut: op == "cut-task" }); 90 | return; 91 | } else if (op == "paste-task") { 92 | if (api._temp && api._temp.length) { 93 | api._temp.forEach((temp: any) => { 94 | api.exec(temp.cut ? "move-task" : "copy-task", { 95 | id: temp.id, 96 | target, 97 | mode: "after", 98 | }); 99 | }); 100 | api._temp = null; 101 | } 102 | return; 103 | } else if (op === "add-task") { 104 | extraData = { 105 | task: { type: "task", text: _("New Task") }, 106 | target, 107 | }; 108 | data = {}; 109 | } else if (op === "edit-task") { 110 | op = "show-editor"; 111 | } else if (op === "convert-task") { 112 | op = "update-task"; 113 | extraData = { task: { type: mode } }; 114 | mode = undefined; 115 | } else if (op === "indent-task") { 116 | mode = mode === "add"; 117 | } 118 | 119 | if (typeof mode !== "undefined") extraData = { mode, ...extraData }; 120 | data = { ...data, ...extraData }; 121 | 122 | api.exec(op, data); 123 | } 124 | 125 | export function isHandledAction( 126 | options: IOptionConfig[] | IButtonConfig[], 127 | id: TID 128 | ): boolean { 129 | return options.some(op => { 130 | if ((op as IOptionConfig).data) 131 | return isHandledAction((op as IOptionConfig).data, id); 132 | return op.id === id; 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /store/src/helpers/menuOptions.ts: -------------------------------------------------------------------------------- 1 | import type { TID } from "wx-lib-state"; 2 | import type { ITask, IGanttTask } from "../types"; 3 | 4 | export interface IOptionConfig { 5 | id?: TID; 6 | separator?: boolean; 7 | text?: string; 8 | icon?: string; 9 | data?: IOptionConfig[]; 10 | type?: string; 11 | check?: (task: ITask, _tasks?: IGanttTask[]) => boolean | TID; 12 | dataFactory?: (obj: any) => IOptionConfig; 13 | } 14 | 15 | export function assignChecks(items: T[]): T[] { 16 | return items.map(item => { 17 | if (item.data) assignChecks(item.data); 18 | 19 | switch (item.id) { 20 | case "add-task:before": 21 | case "move-task:up": 22 | item.check = (task, _tasks) => !isFirstTask(task, _tasks); 23 | break; 24 | case "move-task:down": 25 | item.check = (task, _tasks) => !isLastTask(task, _tasks); 26 | break; 27 | case "indent-task:add": 28 | item.check = (task, _tasks) => 29 | prevTaskID(task, _tasks) !== task.parent; 30 | break; 31 | case "indent-task:remove": 32 | item.check = task => !isRootTask(task); 33 | break; 34 | } 35 | return item; 36 | }); 37 | } 38 | 39 | function isRootTask(task: ITask) { 40 | return task.parent === 0; 41 | } 42 | 43 | function isFirstTask(task: ITask, _tasks: IGanttTask[]): boolean { 44 | return _tasks[0]?.id === task.id; 45 | } 46 | function isLastTask(task: ITask, _tasks: IGanttTask[]): boolean { 47 | return _tasks[_tasks.length - 1]?.id === task.id; 48 | } 49 | function prevTaskID(task: ITask, _tasks: IGanttTask[]): TID { 50 | const taskIndex = _tasks.findIndex(t => t.id === task.id); 51 | return _tasks[taskIndex - 1]?.id ?? task.parent; 52 | } 53 | 54 | const exclude = (v: any) => (task: ITask) => task.type !== v; 55 | 56 | export const defaultMenuOptions: IOptionConfig[] = assignChecks([ 57 | { 58 | id: "add-task", 59 | text: "Add", 60 | icon: "wxi-plus", 61 | data: [ 62 | { id: "add-task:child", text: "Child task" }, 63 | { id: "add-task:before", text: "Task above" }, 64 | { id: "add-task:after", text: "Task below" }, 65 | ], 66 | }, 67 | { type: "separator" }, 68 | { 69 | id: "convert-task", 70 | text: "Convert to", 71 | icon: "wxi-swap-horizontal", 72 | dataFactory: type => { 73 | return { 74 | id: `convert-task:${type.id}`, 75 | text: `${type.label}`, 76 | check: exclude(type.id), 77 | }; 78 | }, 79 | }, 80 | { 81 | id: "edit-task", 82 | text: "Edit", 83 | icon: "wxi-edit", 84 | }, 85 | { id: "cut-task", text: "Cut", icon: "wxi-content-cut" }, 86 | { id: "copy-task", text: "Copy", icon: "wxi-content-copy" }, 87 | { id: "paste-task", text: "Paste", icon: "wxi-content-paste" }, 88 | { 89 | id: "move-task", 90 | text: "Move", 91 | icon: "wxi-swap-vertical", 92 | data: [ 93 | { id: "move-task:up", text: "Up" }, 94 | { id: "move-task:down", text: "Down" }, 95 | ], 96 | }, 97 | { type: "separator" }, 98 | { id: "indent-task:add", text: "Indent", icon: "wxi-indent" }, 99 | { id: "indent-task:remove", text: "Outdent", icon: "wxi-unindent" }, 100 | { type: "separator" }, 101 | { 102 | id: "delete-task", 103 | icon: "wxi-delete", 104 | text: "Delete", 105 | }, 106 | ]); 107 | -------------------------------------------------------------------------------- /store/src/helpers/sort.ts: -------------------------------------------------------------------------------- 1 | import type { IParsedTask, TSort, TSortValue } from "../types"; 2 | export function sort(data: IParsedTask[], conf: TSort) { 3 | return data.sort(sortBy(conf)); 4 | } 5 | 6 | function sortAsc(a: TSortValue, b: TSortValue): number { 7 | if (typeof a === "string") 8 | return a.localeCompare(b as string, undefined, { numeric: true }); 9 | if (typeof a === "object") return a.getTime() - (b as Date).getTime(); 10 | return ((a ?? 0) as number) - ((b ?? 0) as number); 11 | } 12 | 13 | function sortDesc(a: TSortValue, b: TSortValue): number { 14 | if (typeof a === "string") 15 | return -a.localeCompare(b as string, undefined, { numeric: true }); 16 | if (typeof b === "object") return b.getTime() - (a as Date).getTime(); 17 | return ((b ?? 0) as number) - ((a ?? 0) as number); 18 | } 19 | 20 | function sortBy({ key, order }: TSort) { 21 | const sortMethod = order === "asc" ? sortAsc : sortDesc; 22 | return (a: IParsedTask, b: IParsedTask) => sortMethod(a[key], b[key]); 23 | } 24 | -------------------------------------------------------------------------------- /store/src/helpers/toolbarButtons.ts: -------------------------------------------------------------------------------- 1 | import { assignChecks } from "./menuOptions"; 2 | 3 | export interface IButtonConfig { 4 | id?: string; 5 | comp: string; 6 | text?: string; 7 | icon?: string; 8 | type?: string; 9 | menuText?: string; 10 | 11 | check?: (params: any) => boolean; 12 | } 13 | 14 | export const defaultToolbarButtons: IButtonConfig[] = 15 | assignChecks([ 16 | { 17 | id: "add-task", 18 | comp: "button", 19 | icon: "wxi-plus", 20 | text: "New task", 21 | type: "primary", 22 | }, 23 | { 24 | id: "edit-task", 25 | comp: "icon", 26 | icon: "wxi-edit", 27 | menuText: "Edit", 28 | }, 29 | { 30 | id: "delete-task", 31 | comp: "icon", 32 | icon: "wxi-delete", 33 | menuText: "Delete", 34 | }, 35 | { comp: "separator" }, 36 | { 37 | id: "move-task:up", 38 | comp: "icon", 39 | icon: "wxi-angle-up", 40 | menuText: "Move up", 41 | }, 42 | { 43 | id: "move-task:down", 44 | comp: "icon", 45 | icon: "wxi-angle-down", 46 | menuText: "Move down", 47 | }, 48 | { comp: "separator" }, 49 | { 50 | id: "copy-task", 51 | comp: "icon", 52 | icon: "wxi-content-copy", 53 | menuText: "Copy", 54 | }, 55 | { 56 | id: "cut-task", 57 | comp: "icon", 58 | icon: "wxi-content-cut", 59 | menuText: "Cut", 60 | }, 61 | { 62 | id: "paste-task", 63 | comp: "icon", 64 | icon: "wxi-content-paste", 65 | menuText: "Paste", 66 | }, 67 | { comp: "separator" }, 68 | { 69 | id: "indent-task:add", 70 | comp: "icon", 71 | icon: "wxi-indent", 72 | menuText: "Indent", 73 | }, 74 | { 75 | id: "indent-task:remove", 76 | comp: "icon", 77 | icon: "wxi-unindent", 78 | menuText: "Outdent", 79 | }, 80 | ]); 81 | -------------------------------------------------------------------------------- /store/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DataStore } from "./DataStore"; 2 | 3 | export { grid } from "./dom/scales"; 4 | export { placeLink } from "./links"; 5 | export { getDiffer, getAdder, getUnitStart, format } from "./time"; 6 | export { handleAction, isHandledAction } from "./helpers/actionHandlers"; 7 | export { defaultToolbarButtons } from "./helpers/toolbarButtons"; 8 | export { defaultMenuOptions } from "./helpers/menuOptions"; 9 | export { defaultEditorShape } from "./sidebar"; 10 | export { defaultColumns } from "./columns"; 11 | 12 | export type { 13 | TID, 14 | ITask, 15 | ILink, 16 | IZoomConfig, 17 | GanttColumn, 18 | TMethodsConfig, 19 | } from "./types"; 20 | -------------------------------------------------------------------------------- /store/src/links.ts: -------------------------------------------------------------------------------- 1 | import type { IGanttTask, IGanttLink } from "./types"; 2 | 3 | const delta = 20; 4 | 5 | export const updateLink = function ( 6 | link: IGanttLink, 7 | startTask: IGanttTask, 8 | endTask: IGanttTask, 9 | height: number, 10 | baselines: boolean 11 | ): IGanttLink { 12 | const dy = Math.round(height / 2) - 3; 13 | 14 | if ( 15 | !startTask || 16 | !endTask || 17 | !startTask.$y || 18 | !endTask.$y || 19 | startTask.$skip || 20 | endTask.$skip 21 | ) { 22 | link.$p = ""; 23 | return link; 24 | } 25 | 26 | let s_start = false; 27 | let e_start = false; 28 | 29 | switch (link.type) { 30 | case "e2s": 31 | e_start = true; 32 | break; 33 | 34 | case "s2s": 35 | s_start = true; 36 | e_start = true; 37 | break; 38 | 39 | case "s2e": 40 | s_start = true; 41 | break; 42 | 43 | default: 44 | break; 45 | } 46 | 47 | const sx = s_start ? startTask.$x : startTask.$x + startTask.$w; 48 | const sy = baselines ? startTask.$y - 7 : startTask.$y; 49 | const ex = e_start ? endTask.$x : endTask.$x + endTask.$w; 50 | const ey = baselines ? endTask.$y - 7 : endTask.$y; 51 | 52 | if (sx !== ex || sy !== ey) { 53 | const lineCoords = getLineCoords( 54 | sx, 55 | sy + dy, 56 | ex, 57 | ey + dy, 58 | s_start, 59 | e_start, 60 | height / 2, 61 | baselines 62 | ); 63 | 64 | const arrowCoords = getArrowCoords(ex, ey + dy, e_start); 65 | link.$p = `${lineCoords},${arrowCoords}`; 66 | } 67 | 68 | return link; 69 | }; 70 | 71 | function getLineCoords( 72 | sx: number, 73 | sy: number, 74 | ex: number, 75 | ey: number, 76 | s_start: boolean, 77 | e_start: boolean, 78 | gapp: number, 79 | baselines: boolean 80 | ): string { 81 | const shift = delta * (s_start ? -1 : 1); 82 | const backshift = delta * (e_start ? -1 : 1); 83 | 84 | const sx1 = sx + shift; 85 | const ex1 = ex + backshift; 86 | const line = [sx, sy, sx1, sy, 0, 0, 0, 0, ex1, ey, ex, ey]; 87 | 88 | const dx = ex1 - sx1; 89 | let dy = ey - sy; 90 | 91 | const same = e_start === s_start; 92 | if (!same) { 93 | if ((ex1 <= sx + delta - 2 && e_start) || (ex1 > sx && !e_start)) { 94 | dy = baselines ? dy - gapp + 6 : dy - gapp; 95 | } 96 | } 97 | 98 | if ((same && e_start && sx1 > ex1) || (same && !e_start && sx1 < ex1)) { 99 | line[4] = line[2] + dx; 100 | line[5] = line[3]; 101 | line[6] = line[4]; 102 | line[7] = line[5] + dy; 103 | } else { 104 | line[4] = line[2]; 105 | line[5] = line[3] + dy; 106 | line[6] = line[4] + dx; 107 | line[7] = line[5]; 108 | } 109 | 110 | return line.join(","); 111 | } 112 | 113 | function getArrowCoords(x: number, y: number, start: boolean) { 114 | if (start) { 115 | return `${x - 5},${y - 3},${x - 5},${y + 3},${x},${y}`; 116 | } else { 117 | return `${x + 5},${y + 3},${x + 5},${y - 3},${x},${y}`; 118 | } 119 | } 120 | 121 | export function placeLink( 122 | box: { left: number; top: number }, 123 | start: { x: number; y: number }, 124 | end: { x: number; y: number } 125 | ): { width: number; height: number; left: number; top: number; p: string } { 126 | if (start && end) { 127 | const width = end.x - start.x; 128 | const height = end.y - start.y; 129 | const left = (width > 0 ? start.x : end.x) - box.left; 130 | const top = (height > 0 ? start.y : end.y) - box.top; 131 | const p = `${width > 0 ? 0 : -width},${height > 0 ? 0 : -height},${ 132 | width > 0 ? width : 0 133 | },${height > 0 ? height : 0}`; 134 | return { 135 | width: Math.abs(width), 136 | height: Math.abs(height), 137 | left, 138 | top, 139 | p, 140 | }; 141 | } else { 142 | return null; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /store/src/package.ts: -------------------------------------------------------------------------------- 1 | 2 | export function getStamp() { 3 | return { gen: 1 }; 4 | } 5 | -------------------------------------------------------------------------------- /store/src/sidebar.ts: -------------------------------------------------------------------------------- 1 | import type { TEditorShape, IDataConfig } from "./types"; 2 | import { uid } from "wx-lib-state"; 3 | 4 | export const defaultEditorShape: TEditorShape[] = [ 5 | { 6 | key: "text", 7 | type: "text", 8 | label: "Name", 9 | config: { 10 | placeholder: "Add task name", 11 | focus: true, 12 | }, 13 | }, 14 | { 15 | key: "details", 16 | type: "textarea", 17 | label: "Description", 18 | config: { 19 | placeholder: "Add description", 20 | }, 21 | }, 22 | { 23 | key: "type", 24 | type: "select", 25 | label: "Type", 26 | }, 27 | { 28 | key: "start", 29 | type: "date", 30 | label: "Start date", 31 | }, 32 | { 33 | key: "end", 34 | type: "date", 35 | label: "End date", 36 | }, 37 | { 38 | key: "duration", 39 | type: "counter", 40 | label: "Duration", 41 | config: { 42 | min: 1, 43 | max: 100, 44 | }, 45 | }, 46 | { 47 | key: "progress", 48 | type: "slider", 49 | label: "Progress", 50 | }, 51 | { 52 | key: "links", 53 | type: "links", 54 | }, 55 | ]; 56 | 57 | export function normalizeEditor(state: Partial) { 58 | const editorShape = state.editorShape || defaultEditorShape; 59 | 60 | return editorShape.map((field: any) => { 61 | if (field.type === "select" && field.key === "type") { 62 | field.options = state.taskTypes; 63 | } 64 | 65 | field.id = field.id || uid(); 66 | return field; 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /store/src/tasks.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IGanttTask, 3 | GanttScaleData, 4 | IParsedTask, 5 | GanttDataTree, 6 | ITask, 7 | } from "./types"; 8 | import { isEqual } from "date-fns"; 9 | 10 | const baselineHeight = 8; 11 | const baselineTopPadding = 4; 12 | const defaultPadding = 3; 13 | const heightAdjustment = 7; 14 | const baselineAdjustment = baselineHeight + baselineTopPadding; 15 | 16 | export function dragSummaryKids(task: IParsedTask, dx: number) { 17 | if (task.open) { 18 | task.data?.forEach(kid => { 19 | kid.$x += dx; 20 | dragSummaryKids(kid, dx); 21 | }); 22 | } 23 | } 24 | 25 | export function dragSummary( 26 | tasks: GanttDataTree, 27 | task: IParsedTask, 28 | _scales: GanttScaleData, 29 | cellWidth: number 30 | ) { 31 | const summary = tasks.getSummaryId(task.id); 32 | if (summary) { 33 | const pobj = tasks.byId(summary); 34 | const coords = { 35 | xMin: Infinity, 36 | xMax: 0, 37 | }; 38 | getSummaryBarSize(pobj, coords, _scales, cellWidth); 39 | pobj.$x = coords.xMin; 40 | pobj.$w = coords.xMax - coords.xMin; 41 | 42 | dragSummary(tasks, pobj, _scales, cellWidth); 43 | } 44 | } 45 | 46 | function getSummaryBarSize( 47 | task: IParsedTask, 48 | coords: { xMin: number; xMax: number }, 49 | _scales: GanttScaleData, 50 | cellWidth: number 51 | ) { 52 | const { lengthUnit, start } = _scales; 53 | task.data?.forEach(kid => { 54 | if (typeof kid.$x === "undefined") { 55 | kid.$x = Math.round( 56 | _scales.diff(kid.start, start, lengthUnit) * cellWidth 57 | ); 58 | kid.$w = Math.round( 59 | _scales.diff(kid.end, kid.start, lengthUnit, true) * cellWidth 60 | ); 61 | } 62 | const mD = kid.type === "milestone" && kid.$h ? kid.$h / 2 : 0; 63 | if (coords.xMin > kid.$x) { 64 | coords.xMin = kid.$x + mD; 65 | } 66 | const right = kid.$x + kid.$w - mD; 67 | if (coords.xMax < right) { 68 | coords.xMax = right; 69 | } 70 | if (kid.type !== "summary") 71 | getSummaryBarSize(kid, coords, _scales, cellWidth); 72 | }); 73 | } 74 | 75 | export function setSummaryDates( 76 | task: IParsedTask, 77 | tasks?: Partial[] 78 | ): IParsedTask { 79 | let data; 80 | if (tasks) { 81 | data = tasks.filter(t => t.parent == task.id); 82 | } 83 | const copy = { data, ...task }; 84 | if (copy.data?.length) { 85 | copy.data.forEach((kid: IParsedTask) => { 86 | if (tasks || (kid.type != "summary" && kid.data)) 87 | kid = setSummaryDates(kid, tasks); 88 | if (!copy.start || copy.start > kid.start) { 89 | copy.start = new Date(kid.start); 90 | } 91 | if ( 92 | !copy.end || 93 | copy.end < kid.end || 94 | (kid.type === "milestone" && copy.end < kid.start) 95 | ) { 96 | copy.end = new Date(kid.end || kid.start); 97 | } 98 | }); 99 | } else if (task.type === "summary") { 100 | throw Error( 101 | "Summary tasks must have start and end dates if they have no subtasks" 102 | ); 103 | } 104 | 105 | return copy; 106 | } 107 | 108 | export function updateTask( 109 | t: IGanttTask, 110 | i: number, 111 | cellWidth: number, 112 | cellHeight: number, 113 | scales: GanttScaleData, 114 | baselines: boolean 115 | ): IGanttTask { 116 | calculateTaskDimensions( 117 | t, 118 | i, 119 | cellWidth, 120 | cellHeight, 121 | scales, 122 | baselines, 123 | false 124 | ); 125 | 126 | if (baselines) { 127 | calculateTaskDimensions( 128 | t, 129 | i, 130 | cellWidth, 131 | cellHeight, 132 | scales, 133 | baselines, 134 | true 135 | ); 136 | } 137 | 138 | return t; 139 | } 140 | 141 | function calculateTaskDimensions( 142 | t: IGanttTask, 143 | i: number, 144 | cellWidth: number, 145 | cellHeight: number, 146 | scales: GanttScaleData, 147 | baselines: boolean, 148 | isBaseline: boolean 149 | ) { 150 | const { start: scaleStart, end: scaleEnd, lengthUnit, diff } = scales; 151 | const start = (isBaseline ? "base_" : "") + "start"; 152 | const end = (isBaseline ? "base_" : "") + "end"; 153 | const x = "$x" + (isBaseline ? "_base" : ""); 154 | const y = "$y" + (isBaseline ? "_base" : ""); 155 | const w = "$w" + (isBaseline ? "_base" : ""); 156 | const h = "$h" + (isBaseline ? "_base" : ""); 157 | const skip = "$skip" + (isBaseline ? "_baseline" : ""); 158 | 159 | let startDate = t[start]; 160 | let endDate = t[end]; 161 | 162 | if (isBaseline && !startDate) { 163 | t[skip] = true; 164 | return; 165 | } 166 | 167 | if ( 168 | t[start] < scaleStart && 169 | (t[end] < scaleStart || isEqual(t[end], scaleStart)) 170 | ) { 171 | startDate = endDate = scaleStart; 172 | } else if (t[start] > scaleEnd) { 173 | startDate = endDate = scaleEnd; 174 | } 175 | 176 | t[x] = Math.round(diff(startDate, scaleStart, lengthUnit) * cellWidth); 177 | t[y] = isBaseline 178 | ? t.$y + t.$h + baselineTopPadding 179 | : cellHeight * i + defaultPadding; 180 | t[w] = Math.round(diff(endDate, startDate, lengthUnit, true) * cellWidth); 181 | t[h] = isBaseline 182 | ? baselineHeight 183 | : baselines 184 | ? cellHeight - heightAdjustment - baselineAdjustment 185 | : cellHeight - heightAdjustment; 186 | 187 | if (t.type === "milestone") { 188 | t[x] = t[x] - t.$h / 2; 189 | t[w] = t.$h; 190 | 191 | if (isBaseline) { 192 | t[y] = t.$y + baselineHeight; 193 | t[w] = t[h] = t.$h; 194 | } 195 | } 196 | 197 | t[skip] = isEqual(startDate, endDate); 198 | } 199 | -------------------------------------------------------------------------------- /store/test/columns.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { normalizeColumns, defaultColumns } from "../src/columns"; 3 | 4 | describe("columns", () => { 5 | test("normalize column config", () => { 6 | const columns = normalizeColumns(defaultColumns); 7 | 8 | expect(columns.length).to.eq(4); 9 | 10 | for (const col of columns) { 11 | expect(col.width).to.not.be.undefined; 12 | expect(col.align).to.not.be.undefined; 13 | if (col.id === "start" || col.id === "end") 14 | expect(col.template).to.not.be.undefined; 15 | if (col.id === "action") expect(col.action).to.not.be.undefined; 16 | else { 17 | expect(col.resize).to.be.true; 18 | expect(col.sort).to.be.true; 19 | } 20 | } 21 | 22 | expect(normalizeColumns([])).to.deep.eq([]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /store/test/links.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { updateLink } from "../src/links"; 3 | 4 | describe("links", () => { 5 | test("link updating", () => { 6 | const link = { id: 1, source: 1, target: 2, type: "e2s" }; 7 | const startTask = { $x: 100, $y: 3, $w: 300 }; 8 | const endTask = { $x: 200, $y: 41, $w: 200 }; 9 | 10 | const updatedLink = updateLink( 11 | link as any, 12 | startTask as any, 13 | endTask as any, 14 | 38, 15 | false 16 | ); 17 | 18 | expect(updatedLink.id).to.eq(1); 19 | expect(updatedLink.source).to.eq(1); 20 | expect(updatedLink.target).to.eq(2); 21 | expect(updatedLink.type).to.eq("e2s"); 22 | expect(updatedLink.$p).to.eq( 23 | "400,19,420,19,420,38,180,38,180,57,200,57,195,54,195,60,200,57" 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /store/test/scales.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { calcScales, resetScales } from "../src/scales"; 3 | import { getData, scaleHeight, cellWidth } from "./stubs/data"; 4 | import { GanttScaleData } from "../src/types"; 5 | import GanttDataTree from "../src/GanttDataTree"; 6 | 7 | describe("scales", () => { 8 | test("calculate scales", () => { 9 | const { tasks } = getData(); 10 | 11 | const _scales = calcScales( 12 | new Date(2024, 3, 1), 13 | new Date(2024, 3, 10), 14 | "day", 15 | new GanttDataTree(tasks) 16 | ); 17 | 18 | expect(_scales._start).to.deep.eq(new Date(2024, 3, 1)); 19 | expect(_scales._end).to.deep.eq(new Date(2024, 3, 10)); 20 | }); 21 | 22 | test("recalculate scales", () => { 23 | const { scales } = getData(); 24 | 25 | const _scales = resetScales( 26 | new Date(2024, 3, 1), 27 | new Date(2024, 3, 10), 28 | "day", 29 | cellWidth, 30 | scaleHeight, 31 | scales 32 | ) as GanttScaleData; 33 | 34 | expect(_scales.start).to.deep.eq(new Date(2024, 3, 1)); 35 | expect(_scales.end).to.deep.eq(new Date(2024, 3, 10)); 36 | expect(_scales.width).to.eq(900); 37 | expect(_scales.height).to.eq(60); 38 | expect(_scales.lengthUnitWidth).to.eq(100); 39 | expect(_scales.lengthUnit).to.eq("day"); 40 | expect(_scales.minUnit).to.eq("day"); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /store/test/sidebar.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { normalizeEditor } from "../src/sidebar"; 3 | import { taskTypes } from "./stubs/data"; 4 | 5 | describe("sidebar", () => { 6 | test("normalize editor config", () => { 7 | const editorShape = normalizeEditor({ taskTypes }); // assign IDs to fields and set options for available task types 8 | 9 | for (const field of editorShape) { 10 | expect(field.id).to.not.be.undefined; 11 | if (field.type === "select" && field.key === "type") 12 | expect(field.options).to.deep.eq(taskTypes); 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /store/test/stubs/writable.ts: -------------------------------------------------------------------------------- 1 | export function writable(value: any) { 2 | let subscriptions: any[] = []; 3 | const trigger = (b: any) => 4 | subscriptions.forEach((a: any) => { 5 | if (a) a(b); 6 | }); 7 | 8 | return { 9 | subscribe: (handler: any) => { 10 | subscriptions.push(handler); 11 | trigger(value); 12 | 13 | return () => 14 | (subscriptions = subscriptions.filter(a => a != handler)); 15 | }, 16 | set: (nv: any) => { 17 | value = nv; 18 | trigger(value); 19 | }, 20 | update: (cb: any) => { 21 | value = cb(value); 22 | trigger(value); 23 | }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /store/test/tasks.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { updateTask } from "../src/tasks"; 3 | import { resetScales } from "../src/scales"; 4 | import { GanttScaleData } from "../src/types"; 5 | import { getData, cellHeight, cellWidth, scaleHeight } from "./stubs/data"; 6 | 7 | describe("tasks", () => { 8 | test("recalculate task position", () => { 9 | const task = { 10 | id: 1, 11 | text: "Task 1", 12 | type: "task", 13 | start: new Date(2024, 3, 2), 14 | end: new Date(2024, 3, 5), 15 | }; 16 | 17 | const { scales } = getData(); 18 | 19 | const _scales = resetScales( 20 | new Date(2024, 3, 1), 21 | new Date(2024, 3, 10), 22 | "day", 23 | cellWidth, 24 | scaleHeight, 25 | scales 26 | ) as GanttScaleData; 27 | 28 | const updatedTask = updateTask( 29 | task as any, 30 | 0, 31 | cellWidth, 32 | cellHeight, 33 | _scales, 34 | false 35 | ); 36 | 37 | expect(updatedTask.$x).to.eq(100); 38 | expect(updatedTask.$y).to.eq(3); // 0 + default padding 39 | expect(updatedTask.$w).to.eq(300); 40 | expect(updatedTask.$h).to.eq(31); 41 | expect(updatedTask.$skip).to.eq(false); 42 | }); 43 | 44 | test("recalculate task position, milestone", () => { 45 | const task = { 46 | id: 1, 47 | text: "Task 1", 48 | type: "milestone", 49 | start: new Date(2024, 3, 2), 50 | end: new Date(2024, 3, 5), 51 | }; 52 | 53 | const { scales } = getData(); 54 | 55 | const _scales = resetScales( 56 | new Date(2024, 3, 1), 57 | new Date(2024, 3, 10), 58 | "day", 59 | cellWidth, 60 | scaleHeight, 61 | scales 62 | ) as GanttScaleData; 63 | 64 | const updatedTask = updateTask( 65 | task as any, 66 | 0, 67 | cellWidth, 68 | cellHeight, 69 | _scales, 70 | false 71 | ); 72 | 73 | expect(updatedTask.$x).to.eq(84.5); 74 | expect(updatedTask.$y).to.eq(3); // 0 + default padding 75 | expect(updatedTask.$w).to.eq(31); 76 | expect(updatedTask.$h).to.eq(31); 77 | }); 78 | 79 | test("recalculate task position, baselines enabled", () => { 80 | const task = { 81 | id: 1, 82 | text: "Task 1", 83 | type: "task", 84 | start: new Date(2024, 3, 2), 85 | end: new Date(2024, 3, 5), 86 | base_start: new Date(2024, 3, 2), 87 | base_end: new Date(2024, 3, 5), 88 | }; 89 | 90 | const { scales } = getData(); 91 | 92 | const _scales = resetScales( 93 | new Date(2024, 3, 1), 94 | new Date(2024, 3, 10), 95 | "day", 96 | cellWidth, 97 | scaleHeight, 98 | scales 99 | ) as GanttScaleData; 100 | 101 | const updatedTask = updateTask( 102 | task as any, 103 | 0, 104 | cellWidth, 105 | cellHeight, 106 | _scales, 107 | true 108 | ); 109 | 110 | expect(updatedTask.$x).to.eq(100); 111 | expect(updatedTask.$y).to.eq(3); // 0 + default padding 112 | expect(updatedTask.$w).to.eq(300); 113 | expect(updatedTask.$h).to.eq(19); 114 | expect(updatedTask.$skip).to.eq(false); 115 | 116 | expect(updatedTask.$x_base).to.eq(100); 117 | expect(updatedTask.$y_base).to.eq(26); 118 | expect(updatedTask.$w_base).to.eq(300); 119 | expect(updatedTask.$h_base).to.eq(8); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /store/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "esnext", 5 | "lib": ["ESNext", "DOM"], 6 | "target": "ESNext", 7 | "types": ["vite/client"], 8 | 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "allowJs": true, 15 | "checkJs": false, 16 | 17 | "noImplicitAny": true, 18 | "sourceMap": true, 19 | 20 | "baseUrl": ".", 21 | "declaration": true, 22 | "outDir": "dist/" 23 | }, 24 | "watchOptions": {}, 25 | "include": ["src/**/*.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /store/vite.config.js: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { resolve } from "path"; 3 | import { defineConfig, loadEnv } from "vite"; 4 | import dts from "vite-plugin-dts"; 5 | import conditionalCompile from "vite-plugin-conditional-compile"; 6 | import esbuild from "esbuild"; 7 | 8 | const minify = { 9 | name: "minify", 10 | closeBundle: () => { 11 | esbuild.buildSync({ 12 | entryPoints: ["./dist/index.js"], 13 | minify: true, 14 | allowOverwrite: true, 15 | outfile: "./dist/index.js", 16 | }); 17 | }, 18 | }; 19 | 20 | export default function ({ mode }) { 21 | process.env = { ...process.env, ...loadEnv(mode, process.cwd(), "WX") }; 22 | const trial = !!process.env.WX_TRIAL_PACKAGE; 23 | 24 | const config = { 25 | build: { 26 | lib: { 27 | entry: resolve(__dirname, "src/index.ts"), 28 | name: "store", 29 | formats: ["es"], 30 | fileName: () => `index.js`, 31 | }, 32 | sourcemap: !trial, 33 | minify: mode !== "development", 34 | target: "esnext", 35 | }, 36 | test: { 37 | coverage: { 38 | reporter: ["text"], 39 | }, 40 | }, 41 | plugins: [], 42 | }; 43 | 44 | if (mode !== "development") { 45 | config.plugins.push(conditionalCompile({})); 46 | } 47 | 48 | if (!trial) { 49 | config.plugins.push(dts({ outDir: resolve(__dirname, "dist/types") })); 50 | } else { 51 | config.plugins.push(dts({ outDir: resolve(__dirname, "dist/types") })); 52 | } 53 | if (mode !== "development") { 54 | config.plugins.push(minify); 55 | } 56 | 57 | return defineConfig(config); 58 | } 59 | -------------------------------------------------------------------------------- /svelte/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | cypress/screenshots 3 | cypress/videos 4 | -------------------------------------------------------------------------------- /svelte/cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | video: false, 5 | e2e: { 6 | setupNodeEvents() {}, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /svelte/cypress/e2e/basic/scales.cy.js: -------------------------------------------------------------------------------- 1 | context("Scale", () => { 2 | it("scale works with local data", () => { 3 | cy.visit("/index.html#/zoom/willow"); 4 | cy.viewport(1300, 900); 5 | 6 | let cellWidth = 100; 7 | const zoomSteps = 6; 8 | const zoomDelta = 50; // each zoom step is 50px 9 | const initialZoomStep = cellWidth / zoomDelta; // get initial zoom step 10 | const minCellWidth = 50; 11 | const maxCellWidth = 300; 12 | 13 | const scroll = zoom => { 14 | cy.get(".wx-chart").trigger("wheel", { 15 | deltaY: zoom === "zoom-in" ? -100 : 100, 16 | ctrlKey: true, 17 | }); 18 | }; 19 | cy.get(".wx-scale .wx-row").first().as("topRowScale"); 20 | cy.get(".wx-scale .wx-row:last-child > :first-child").should( 21 | "have.css", 22 | "width", 23 | `${cellWidth}px` 24 | ); 25 | 26 | for (let i = initialZoomStep; i <= zoomSteps; i++) { 27 | cellWidth = 28 | cellWidth + zoomDelta > maxCellWidth 29 | ? minCellWidth 30 | : cellWidth + zoomDelta; 31 | 32 | scroll("zoom-in"); 33 | cy.get(".wx-scale .wx-row:last-child > :first-child").should( 34 | "have.css", 35 | "width", 36 | `${cellWidth}px` 37 | ); 38 | 39 | i < zoomSteps 40 | ? cy.get("@topRowScale").should("contain", "April 2024") 41 | : cy.get("@topRowScale").should("contain", "Apr 6"); 42 | } 43 | 44 | cy.shot("zoom-in works"); 45 | 46 | cy.get("@topRowScale").should("contain", "Apr 6"); 47 | for (let i = initialZoomStep; i <= zoomSteps; i++) { 48 | cellWidth = 49 | cellWidth - zoomDelta < minCellWidth 50 | ? maxCellWidth 51 | : cellWidth - zoomDelta; 52 | 53 | scroll("zoom-out"); 54 | cy.get(".wx-scale .wx-row:last-child > :first-child").should( 55 | "have.css", 56 | "width", 57 | `${cellWidth}px` 58 | ); 59 | 60 | cy.get("@topRowScale").should("contain", "April 2024"); 61 | } 62 | 63 | cy.shot("zoom-out works"); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /svelte/cypress/e2e/demos.cy.js: -------------------------------------------------------------------------------- 1 | const cases = [ 2 | "/base/:skin", 3 | "/locale/:skin", 4 | "/tooltips/:skin", 5 | "/fullscreen/:skin", 6 | "/templates/:skin", 7 | "/markers/:skin", 8 | "/holidays/:skin", 9 | "/no-grid/:skin", 10 | "/grid-fill-space-columns/:skin", 11 | "/grid-fixed-columns/:skin", 12 | "/grid-custom-columns/:skin", 13 | "/toolbar/:skin", 14 | "/toolbar-buttons/:skin", 15 | "/context-menu/:skin", 16 | "/menu-handler/:skin", 17 | "/menu-options/:skin", 18 | "/custom-form-controls/:skin", 19 | "/custom-edit-form/:skin", 20 | "/baseline/:skin", 21 | "/cell-borders/:skin", 22 | "/sizes/:skin", 23 | "/scales/:skin", 24 | "/prevent-actions/:skin", 25 | "/readonly/:skin", 26 | "/performance/:skin", 27 | "/gantt-multiple/:skin", 28 | "/start-end/:skin", 29 | "/zoom/:skin", 30 | "/custom-zoom/:skin", 31 | "/length-unit/:skin", 32 | "/task-types/:skin", 33 | //"/backend/:skin", 34 | //"/backend-provider/:skin", 35 | "/sorting/:skin", 36 | "/sorting-api/:skin", 37 | ]; 38 | 39 | const skins = ["material", "willow", "willow-dark"]; 40 | const links = []; 41 | 42 | cases.forEach(w => { 43 | skins.forEach(s => { 44 | links.push(w.replace(":skin", s)); 45 | }); 46 | }); 47 | 48 | context("Basic functionality", () => { 49 | it("widget", () => { 50 | links.forEach(w => { 51 | cy.visit(`/index.html#${w}`); 52 | cy.wait(500); 53 | cy.shot(w, { area: ".content" }); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /svelte/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /svelte/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | Cypress.Commands.add("shot", (...args) => { 12 | // eslint-disable-next-line cypress/no-unnecessary-waiting 13 | cy.wait(100); 14 | 15 | const name = args.filter(a => typeof a !== "object").join("-"); 16 | const conf = 17 | typeof args[args.length - 1] === "object" ? args[args.length - 1] : {}; 18 | const sconf = { ...conf, overwrite: true }; 19 | 20 | if (conf.area) cy.get(conf.area).screenshot(name, sconf); 21 | else cy.screenshot(name, sconf); 22 | }); 23 | 24 | Cypress.Commands.add( 25 | "clickNoScroll", 26 | { 27 | prevSubject: "element", 28 | }, 29 | subject => { 30 | cy.wrap(subject).click({ scrollBehavior: false }); 31 | } 32 | ); 33 | 34 | Cypress.Commands.add( 35 | "wxG", 36 | { 37 | prevSubject: "optional", 38 | }, 39 | (subject, type, id, side) => { 40 | subject = subject ? cy.wrap(subject) : cy; 41 | switch (type) { 42 | case "toolbar": 43 | //[fixme] change on wx-toolbar after update svelte-toolbar version 44 | return subject.get(".wx-toolbar"); 45 | case "toolbar-button": 46 | return subject.get( 47 | `.wx-toolbar .wx-tb-element[data-id="${id}"]` 48 | ); 49 | case "grid": 50 | return subject.get(".wx-grid"); 51 | case "grid-header": 52 | return subject.get(".wx-grid .wx-h-row"); 53 | case "grid-task-list": 54 | return subject.get(".wx-grid .wx-data"); 55 | case "grid-item": 56 | return subject.get(`.wx-grid .wx-row[data-id="${id}"]`); 57 | 58 | case "editor": 59 | return subject.get(".wx-sidebar"); 60 | case "chart-task-list": 61 | return subject.get(".wx-chart .wx-bars"); 62 | case "chart-link-list": 63 | return subject.get(".wx-chart .wx-links"); 64 | case "chart-item": 65 | return subject.get(`.wx-bar[data-id="${id}"]`); 66 | case "chart-selected-line": 67 | return subject.get(`.wx-area > .wx-selected[data-id="${id}"]`); 68 | case "link": 69 | return subject 70 | .get(`.wx-bar[data-id="${id}"]`) 71 | .find(`.wx-link.wx-${side}`); 72 | case "menu": 73 | return subject.get(".wx-menu"); 74 | case "menu-option": 75 | return subject.get(`.wx-menu .wx-item[data-id="${id}"]`); 76 | 77 | default: 78 | throw `not supported arguments for wxG: ${type}, ${id}`; 79 | } 80 | } 81 | ); 82 | 83 | Cypress.Commands.add("findRootRows", () => { 84 | cy.wxG("grid-task-list") 85 | .find(".wx-row") 86 | .filter((_, el) => { 87 | return ( 88 | Cypress.$(el).find(".wx-content").css("padding-left") === "0px" 89 | ); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /svelte/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /svelte/demos/cases/BasicInit.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /svelte/demos/cases/ChartBorders.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |
18 |
Chart cell borders
19 | 20 |
21 | 22 |
23 | 30 |
31 |
32 | 33 | 62 | -------------------------------------------------------------------------------- /svelte/demos/cases/ContextMenu.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 18 | 19 | -------------------------------------------------------------------------------- /svelte/demos/cases/ContextMenuHandler.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 35 | 36 | -------------------------------------------------------------------------------- /svelte/demos/cases/ContextMenuOptions.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 44 | 45 | -------------------------------------------------------------------------------- /svelte/demos/cases/DropDownMenu.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 |
25 |
26 | 28 |
29 | 30 |
31 | 38 |
39 |
40 | 41 | 60 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttBackend.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttBaseline.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 38 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttBatchProvider.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttCustomSort.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
39 |
40 |
Sort by
41 | 44 | 47 | 50 |
51 | 52 |
53 | 60 |
61 |
62 | 63 | 96 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttCustomZoom.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |

Point over Gantt chart, then hold Ctrl and use mouse wheel to zoom

12 |
13 | 19 |
20 |
21 | 22 | 36 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttFixedColumns.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 34 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttFlexColumns.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 39 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttForm.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
39 | 46 | 47 | {#if task} 48 |
49 | {/if} 50 |
51 | 52 | 58 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttFormControls.svelte: -------------------------------------------------------------------------------- 1 | 50 | 51 | 58 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttFullscreen.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |

Click the "expand" icon, or click on Gantt and press Ctrl+Shift+F

11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 32 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttGrid.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttHolidays.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | -------------------------------------------------------------------------------- /svelte/demos/cases/GanttLengthUnit.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 |
57 |
58 |