├── .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 | [](https://www.npmjs.com/package/wx-svelte-gantt)
16 | [](https://github.com/svar-widgets/gantt/blob/main/license.txt)
17 | [](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 |
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 |
36 | {:else}
37 |
38 | {/if}
39 |
40 |
41 |
42 |
43 |
76 |
--------------------------------------------------------------------------------
/site/src/Main.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
20 |
21 | {#key skin}
22 |
23 | {/key}
24 |
25 |
26 |
27 |
51 |
--------------------------------------------------------------------------------
/site/src/MainDemo.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
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 |
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 | menu.show(ev)}>Task action
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 |
sort("text")}
42 | >Task Name
44 |
sort("start")}
45 | >Start Date
47 |
sort("duration")}
48 | >Duration
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 |
59 |
60 |
61 |
69 |
70 |
71 |
72 |
92 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttLocale.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 | {#if lang === "en"}
26 |
27 |
28 |
29 |
36 |
37 |
38 | {/if}
39 |
40 | {#if lang === "cn"}
41 |
42 |
43 |
44 |
45 |
52 |
53 |
54 |
55 | {/if}
56 |
57 |
58 |
78 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttMarkers.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
34 |
35 |
36 |
44 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttMultiple.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 | Add Gantt
33 |
34 |
35 | {#each gantts as gantt}
36 |
50 | {/each}
51 |
52 |
53 |
81 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttNoGrid.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
18 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttPerformance.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 | {#if start}
23 | 10 000 tasks (
24 | {years}
25 | years ) rendered in
26 |
27 | ms
28 | {:else}
29 | (start = new Date())}>
30 | Press me to render Gantt chart with 10 000 tasks
31 |
32 | {/if}
33 |
34 |
35 | {#if start}
36 |
37 |
43 |
44 | {/if}
45 |
46 |
47 |
75 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttPreventActions.svelte:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 | {#snippet children({ id })}
46 |
47 | {/snippet}
48 |
49 |
50 | {#snippet children({ id })}
51 |
52 | {/snippet}
53 |
54 |
55 | {#snippet children({ id })}
56 |
57 | {/snippet}
58 |
59 |
60 | {#snippet children({ id })}
61 |
62 | {/snippet}
63 |
64 |
65 |
71 |
79 |
80 |
81 |
82 |
122 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttProvider.svelte:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttReadOnly.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
17 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttScales.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
19 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttSizes.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
37 |
38 |
39 |
40 |
61 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttSort.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
Sorting by the "Task Name" column only
18 |
19 |
26 |
27 |
28 |
29 |
43 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttStartEnd.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 | {#snippet children({ id })}
19 |
20 | {/snippet}
21 |
22 |
23 | {#snippet children({ id })}
24 |
25 | {/snippet}
26 |
27 |
28 |
29 |
30 |
31 |
40 |
41 |
42 |
43 |
62 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttSummariesConvert.svelte:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 |
60 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttSummariesNoDrag.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttSummariesProgress.svelte:
--------------------------------------------------------------------------------
1 |
110 |
111 |
112 |
113 |
114 |
123 |
124 |
125 |
126 |
127 |
132 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttTaskTypes.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
18 |
19 |
20 |
77 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttText.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
30 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttToolbar.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
21 |
22 |
23 |
29 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttToolbarButtons.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
39 |
40 |
41 |
47 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttToolbarCustom.svelte:
--------------------------------------------------------------------------------
1 |
82 |
83 |
84 |
85 |
92 |
93 |
94 |
100 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttTooltips.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
20 |
21 |
--------------------------------------------------------------------------------
/svelte/demos/cases/GanttZoom.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
Point over Gantt chart, then hold Ctrl and use mouse wheel to zoom
18 |
19 |
27 |
28 |
29 |
30 |
44 |
--------------------------------------------------------------------------------
/svelte/demos/common/Link.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 | {data[1]}
10 |
11 |
12 |
33 |
--------------------------------------------------------------------------------
/svelte/demos/common/ListRoutes.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
7 | {#each routes as route}
8 | {'"' + route + '",\n'}
9 | {/each}
10 |
12 |
13 |
18 |
--------------------------------------------------------------------------------
/svelte/demos/common/Router.svelte:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/svelte/demos/common/helpers.js:
--------------------------------------------------------------------------------
1 | import { push } from "svelte-spa-router";
2 | import { wrap } from "svelte-spa-router/wrap";
3 | import { links as raw } from "../routes";
4 |
5 | const routes = {
6 | "/": wrap({
7 | component: {},
8 | conditions: () => {
9 | push("/base/willow");
10 | return false;
11 | },
12 | }),
13 | };
14 |
15 | function getRoutes(skinSettings, cb) {
16 | raw.forEach(
17 | a =>
18 | (routes[a[0]] = wrap({
19 | component: a[2],
20 | userData: a,
21 | props: { ...skinSettings },
22 | conditions: x => {
23 | cb(x.location);
24 | return true;
25 | },
26 | }))
27 | );
28 |
29 | return routes;
30 | }
31 |
32 | function getLinks() {
33 | return raw;
34 | }
35 |
36 | export { push, getRoutes, getLinks };
37 |
--------------------------------------------------------------------------------
/svelte/demos/custom/AvatarCell.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 | {#if row.assigned}
13 |
14 |
15 |
16 | {/if}
17 |
18 |
{row.assigned}
19 |
20 |
21 |
48 |
--------------------------------------------------------------------------------
/svelte/demos/custom/Form.svelte:
--------------------------------------------------------------------------------
1 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
64 |
65 |
Name
66 |
67 |
68 |
Description
69 |
70 |
71 | {#if taskTypes.length > 1}
72 |
Type
73 |
74 | {/if}
75 |
76 |
Start date
77 |
78 |
79 | {#if task.type !== "milestone"}
80 |
End date
81 |
82 |
86 | {/if}
87 |
88 |
Delete
91 |
92 |
93 |
94 |
95 |
96 |
169 |
--------------------------------------------------------------------------------
/svelte/demos/custom/MyTaskContent.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 | {#if data.type !== "milestone"}
17 | {data.text || ""}
18 |
19 | {#if data.clicked}Was clicked{:else}Click Me{/if}
20 |
21 | {:else}
22 | {data.text || ""}
23 | {/if}
24 |
25 |
50 |
--------------------------------------------------------------------------------
/svelte/demos/custom/MyTooltipContent.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 |
--------------------------------------------------------------------------------
/svelte/demos/custom/placeholder.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svar-widgets/gantt/6cef8d80921105007ee4494f3c9a5e3d286cf049/svelte/demos/custom/placeholder.md
--------------------------------------------------------------------------------
/svelte/demos/index.js:
--------------------------------------------------------------------------------
1 | import { mount } from "svelte";
2 | import Demos from "./common/Index.svelte";
3 |
4 | mount(Demos, {
5 | target: document.querySelector("#wx_demo_area") || document.body,
6 | });
7 |
--------------------------------------------------------------------------------
/svelte/demos/routes.js:
--------------------------------------------------------------------------------
1 | import BasicInit from "./cases/BasicInit.svelte";
2 | import GanttProvider from "./cases/GanttProvider.svelte";
3 | import GanttBatchProvider from "./cases/GanttBatchProvider.svelte";
4 | import GanttBackend from "./cases/GanttBackend.svelte";
5 | import GanttScales from "./cases/GanttScales.svelte";
6 | import GanttGrid from "./cases/GanttGrid.svelte";
7 | import GanttNoGrid from "./cases/GanttNoGrid.svelte";
8 | import GanttFixedColumns from "./cases/GanttFixedColumns.svelte";
9 | import GanttFlexColumns from "./cases/GanttFlexColumns.svelte";
10 | import GanttReadOnly from "./cases/GanttReadOnly.svelte";
11 | import GanttPreventActions from "./cases/GanttPreventActions.svelte";
12 | import GanttFormControls from "./cases/GanttFormControls.svelte";
13 | import GanttForm from "./cases/GanttForm.svelte";
14 | import GanttSizes from "./cases/GanttSizes.svelte";
15 | import GanttMultiple from "./cases/GanttMultiple.svelte";
16 | import GanttPerformance from "./cases/GanttPerformance.svelte";
17 |
18 |
19 | import GanttTooltips from "./cases/GanttTooltips.svelte";
20 | import GanttToolbar from "./cases/GanttToolbar.svelte";
21 | import GanttToolbarCustom from "./cases/GanttToolbarCustom.svelte";
22 | import GanttToolbarButtons from "./cases/GanttToolbarButtons.svelte";
23 | import GanttText from "./cases/GanttText.svelte";
24 | import GanttLocale from "./cases/GanttLocale.svelte";
25 | import GanttStartEnd from "./cases/GanttStartEnd.svelte";
26 | import GanttFullscreen from "./cases/GanttFullscreen.svelte";
27 | import GanttZoom from "./cases/GanttZoom.svelte";
28 | import GanttCustomZoom from "./cases/GanttCustomZoom.svelte";
29 | import GanttLengthUnit from "./cases/GanttLengthUnit.svelte";
30 | import GanttTaskTypes from "./cases/GanttTaskTypes.svelte";
31 | import GanttBaseline from "./cases/GanttBaseline.svelte";
32 | import ChartCellBorders from "./cases/ChartBorders.svelte";
33 | import ContextMenu from "./cases/ContextMenu.svelte";
34 | import ContextMenuHandler from "./cases/ContextMenuHandler.svelte";
35 | //import DropDownMenu from "./cases/DropDownMenu.svelte";
36 | import ContextMenuOptions from "./cases/ContextMenuOptions.svelte";
37 | import GanttHolidays from "./cases/GanttHolidays.svelte";
38 | import GanttSort from "./cases/GanttSort.svelte";
39 | import GanttCustomSort from "./cases/GanttCustomSort.svelte";
40 | import GanttSummariesProgress from "./cases/GanttSummariesProgress.svelte";
41 | import GanttSummariesNoDrag from "./cases/GanttSummariesNoDrag.svelte";
42 | import GanttSummariesConvert from "./cases/GanttSummariesConvert.svelte";
43 |
44 | export const links = [
45 | ["/base/:skin", "Basic Gantt", BasicInit],
46 |
47 | ["/sizes/:skin", "Scale / cell sizes", GanttSizes],
48 | ["/cell-borders/:skin", "Chart cell borders", ChartCellBorders],
49 | ["/scales/:skin", "Custom scales", GanttScales],
50 | ["/start-end/:skin", "Start/end dates", GanttStartEnd],
51 |
52 |
53 | ["/holidays/:skin", "Holidays", GanttHolidays],
54 | ["/baseline/:skin", "Baselines", GanttBaseline],
55 | ["/templates/:skin", "Custom text", GanttText],
56 | ["/tooltips/:skin", "Tooltips", GanttTooltips],
57 |
58 | ["/task-types/:skin", "Task types", GanttTaskTypes],
59 | [
60 | "/summary-progress/:skin",
61 | "Summary tasks with auto progress",
62 | GanttSummariesProgress,
63 | ],
64 | [
65 | "/summary-no-drag/:skin",
66 | "No drag for summary tasks",
67 | GanttSummariesNoDrag,
68 | ],
69 | [
70 | "/summary-convert/:skin",
71 | "Auto convert to summary tasks",
72 | GanttSummariesConvert,
73 | ],
74 |
75 | ["/zoom/:skin", "Zoom", GanttZoom],
76 | ["/custom-zoom/:skin", "Custom Zoom", GanttCustomZoom],
77 | ["/length-unit/:skin", "Length unit (rounding)", GanttLengthUnit],
78 |
79 | ["/no-grid/:skin", "No grid", GanttNoGrid],
80 | [
81 | "/grid-fill-space-columns/:skin",
82 | "Flexible grid columns",
83 | GanttFlexColumns,
84 | ],
85 | ["/grid-fixed-columns/:skin", "Fixed grid columns", GanttFixedColumns],
86 | ["/grid-custom-columns/:skin", "Custom grid columns", GanttGrid],
87 |
88 | ["/toolbar/:skin", "Toolbar", GanttToolbar],
89 | ["/toolbar-buttons/:skin", "Toolbar: limited buttons", GanttToolbarButtons],
90 | ["/toolbar-custom/:skin", "Toolbar: custom buttons", GanttToolbarCustom],
91 | ["/context-menu/:skin", "Context menu", ContextMenu],
92 | [
93 | "/menu-handler/:skin",
94 | "Context menu: limiting options",
95 | ContextMenuHandler,
96 | ],
97 | //["/outer-menu/:skin", "Dropdown menu", DropDownMenu],
98 | ["/menu-options/:skin", "Context menu: custom options", ContextMenuOptions],
99 | [
100 | "/custom-form-controls/:skin",
101 | "Editor: custom controls",
102 | GanttFormControls,
103 | ],
104 | ["/custom-edit-form/:skin", "Custom editor", GanttForm],
105 |
106 | ["/locale/:skin", "Locales", GanttLocale],
107 | ["/fullscreen/:skin", "Fullscreen", GanttFullscreen],
108 | ["/readonly/:skin", "Readonly mode", GanttReadOnly],
109 |
110 | ["/prevent-actions/:skin", "Preventing actions", GanttPreventActions],
111 | ["/gantt-multiple/:skin", "Many Gantts per page", GanttMultiple],
112 | ["/performance/:skin", "Performance", GanttPerformance],
113 |
114 | ["/sorting/:skin", "Custom sorting", GanttSort],
115 | ["/sorting-api/:skin", "Sort by API", GanttCustomSort],
116 |
117 | ["/backend/:skin", "Backend data", GanttBackend],
118 | ["/backend-provider/:skin", "Saving to backend", GanttProvider],
119 | [
120 | "/backend-provider-batch/:skin",
121 | "Saving to backend (batch)",
122 | GanttBatchProvider,
123 | ],
124 | ];
125 |
--------------------------------------------------------------------------------
/svelte/demos/skins.js:
--------------------------------------------------------------------------------
1 | export const skins = [
2 | {
3 | id: "willow",
4 | name: "Willow",
5 | props: {},
6 | },
7 | {
8 | id: "willow-dark",
9 | name: "Dark",
10 | props: {},
11 | },
12 | ];
13 |
--------------------------------------------------------------------------------
/svelte/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Widgets
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/svelte/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wx-svelte-gantt",
3 | "version": "2.1.1",
4 | "description": "Interactive and customizable Gantt chart component written in Svelte",
5 | "productTag": "gantt",
6 | "productTrial": false,
7 | "type": "module",
8 | "scripts": {
9 | "build": "vite build",
10 | "build:dist": "vite build --mode dist",
11 | "build:tests": "vite build --mode test",
12 | "lint": "yarn eslint ./demos ./src",
13 | "start": "vite --open",
14 | "start:tests": "vite --open=/tests/ --host 0.0.0.0 --port 5100 --mode test",
15 | "test": "true",
16 | "test:cypress": "cypress run -P ./ --config \"baseUrl=http://localhost:5100/tests\""
17 | },
18 | "svelte": "src/index.js",
19 | "exports": {
20 | ".": {
21 | "svelte": "./src/index.js"
22 | },
23 | "./package.json": "./package.json"
24 | },
25 | "license": "GPL-3.0",
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/svar-widgets/gantt.git"
29 | },
30 | "bugs": {
31 | "url": "https://forum.svar.dev"
32 | },
33 | "homepage": "https://svar.dev/svelte/gantt/",
34 | "dependencies": {
35 | "wx-gantt-data-provider": "2.1.1",
36 | "wx-gantt-locales": "2.1.1",
37 | "wx-gantt-store": "2.1.1",
38 | "wx-lib-dom": "0.8.0",
39 | "wx-lib-state": "1.9.0",
40 | "wx-lib-svelte": "0.5.1",
41 | "wx-svelte-core": "2.1.0",
42 | "wx-svelte-menu": "2.1.0",
43 | "wx-svelte-grid": "2.1.0",
44 | "wx-svelte-toolbar": "2.1.0"
45 | },
46 | "files": [
47 | "src",
48 | "readme.md",
49 | "whatsnew.md",
50 | "license.txt"
51 | ],
52 | "keywords": [
53 | "svelte",
54 | "gantt",
55 | "gantt-chart",
56 | "svelte-gantt",
57 | "svelte-gantt-chart",
58 | "gantt chart",
59 | "gantt-chart-component",
60 | "gantt-diagram",
61 | "svelte-gantt-component",
62 | "svar widgets",
63 | "svar gantt",
64 | "timeline"
65 | ]
66 | }
67 |
--------------------------------------------------------------------------------
/svelte/postcss.config.js:
--------------------------------------------------------------------------------
1 | import autoprefixer from "autoprefixer";
2 |
3 | const plugins = [autoprefixer];
4 | export { plugins };
5 |
--------------------------------------------------------------------------------
/svelte/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # SVAR Svelte Gantt Chart
4 |
5 |
6 |
7 |
8 |
9 | [Website](https://svar.dev/svelte/gantt/) • [Getting Started](https://docs.svar.dev/svelte/gantt/getting_started/) • [Demos](https://docs.svar.dev/svelte/gantt/samples/#/base/willow)
10 |
11 |
12 |
13 |
14 |
15 | [](https://www.npmjs.com/package/wx-svelte-gantt)
16 | [](https://github.com/svar-widgets/gantt/blob/main/license.txt)
17 | [](https://www.npmjs.com/package/wx-svelte-gantt)
18 | [](https://github.com/svar-widgets/gantt)
19 |
20 |
21 |
22 | [SVAR Svelte Gantt](https://svar.dev/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.
23 |
24 |
25 |
26 |
27 |
28 |
29 | # :sparkles: Key Features
30 |
31 | - Interactive drag-and-drop interface
32 | - Intuitive and customizable task edit form
33 | - Set task dependencies on the timeline or in a popup form
34 | - Hierarchical view of sub tasks
35 | - Reordering tasks in grid with drag-and-drop
36 | - Configurable timeline (hours, days, weeks)
37 | - Zooming with scroll
38 | - Showing task progress on the taskbar
39 | - Toolbar and context menu
40 | - Tooltips for taskbars
41 | - Fast performance with big data sets
42 |
43 | # :hammer_and_wrench: How to Use
44 |
45 | To use the widget, simply import the package and include the component in your Svelte file:
46 |
47 | ```svelte
48 |
70 |
71 |
72 | ```
73 | For further instructions, follow the deatailed [how-to-start guide](https://docs.svar.dev/svelte/gantt/getting_started/).
74 |
75 | ### :computer: How to Modify
76 |
77 | Typically, you don't need to modify the code. However, if you wish to do so, follow these steps:
78 |
79 | 1. Run `yarn` to install dependencies. Note that this project is a monorepo using `yarn` workspaces, so npm will not work
80 | 2. Start the project in development mode with `yarn start`
81 |
82 | ### :white_check_mark: Run Tests
83 |
84 | To run the test:
85 |
86 | 1. Start the test examples with:
87 | ```sh
88 | yarn start:tests
89 | ```
90 | 2. In a separate console, run the end-to-end tests with:
91 | ```sh
92 | yarn test:cypress
93 | ```
94 |
95 | ### :speech_balloon: Need Help?
96 |
97 | Join our [community forum](https://forum.svar.dev) to get help and submit feature requests.
98 |
99 |
--------------------------------------------------------------------------------
/svelte/src/components/ContextMenu.svelte:
--------------------------------------------------------------------------------
1 |
121 |
122 |
131 |
132 |
133 | {@render children?.()}
134 |
135 |
136 |
145 |
--------------------------------------------------------------------------------
/svelte/src/components/Fullscreen.svelte:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 | {@render children?.()}
43 |
44 | toggleFullscreen()}
48 | />
49 |
50 |
51 |
52 |
66 |
--------------------------------------------------------------------------------
/svelte/src/components/Gantt.svelte:
--------------------------------------------------------------------------------
1 |
131 |
132 |
133 |
141 |
142 |
--------------------------------------------------------------------------------
/svelte/src/components/Resizer.svelte:
--------------------------------------------------------------------------------
1 |
69 |
70 |
71 |
72 |
81 |
82 |
100 |
--------------------------------------------------------------------------------
/svelte/src/components/TimeScale.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | {#each $scales.rows as row}
12 |
13 | {#each row.cells as cell}
14 |
20 | {cell.value}
21 |
22 | {/each}
23 |
24 | {/each}
25 |
26 |
27 |
58 |
--------------------------------------------------------------------------------
/svelte/src/components/Toolbar.svelte:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/svelte/src/components/chart/CellGrid.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
32 |
--------------------------------------------------------------------------------
/svelte/src/components/chart/Links.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {#each $links as link (link.id)}
11 |
12 | {/each}
13 |
14 |
15 |
33 |
--------------------------------------------------------------------------------
/svelte/src/components/grid/ActionCell.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 | {#if cell || column.action == "add-task"}
9 |
10 |
11 |
12 | {/if}
13 |
14 |
27 |
--------------------------------------------------------------------------------
/svelte/src/components/grid/TextCell.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | {#if row.data || row.lazy}
13 |
17 | {:else}
{/if}
18 |
{row.text}
19 |
20 |
21 |
53 |
--------------------------------------------------------------------------------
/svelte/src/components/sidebar/Links.svelte:
--------------------------------------------------------------------------------
1 |
49 |
50 | {#each linksData as links}
51 | {#if links.data.length}
52 |
53 |
54 |
55 |
56 |
57 |
58 | {#each links.data as obj}
59 |
60 |
61 |
62 | {obj.task.text || ""}
63 |
64 |
65 |
66 |
67 |
68 |
73 | onChange(e, obj.link.id)}
74 | >
75 | {#snippet children({ option })}
76 | {option.label}
77 | {/snippet}
78 |
79 |
80 |
81 |
82 |
83 |
88 |
89 |
90 | {/each}
91 |
92 |
93 |
94 |
95 | {/if}
96 | {/each}
97 |
98 |
138 |
--------------------------------------------------------------------------------
/svelte/src/helpers/hotkey.js:
--------------------------------------------------------------------------------
1 | class ScreenKeys {
2 | constructor() {
3 | this.store = new Map();
4 | }
5 | limit(check) {
6 | this._scope = check;
7 | }
8 | isActive() {
9 | return !this._scope || this._scope();
10 | }
11 | add(key, handler) {
12 | this.store.set(key.toLowerCase().replace(/[ ]/g, ""), handler);
13 | }
14 | }
15 |
16 | const chain = [];
17 | export const hotkeys = {
18 | subscribe: v => {
19 | init_once();
20 |
21 | const t = new ScreenKeys();
22 | chain.push(t);
23 | v(t);
24 |
25 | return () => {
26 | const ind = chain.findIndex(a => a === t);
27 | if (ind >= 0) chain.splice(ind, 1);
28 | };
29 | },
30 | };
31 |
32 | var ready = false;
33 | function init_once() {
34 | if (ready) return;
35 | ready = true;
36 |
37 | document.addEventListener("keydown", ev => {
38 | if (
39 | chain.length &&
40 | (ev.ctrlKey ||
41 | ev.altKey ||
42 | ev.metaKey ||
43 | ev.shiftKey ||
44 | ev.key.length > 1 ||
45 | ev.key === " ")
46 | ) {
47 | const code = [];
48 | if (ev.ctrlKey) code.push("ctrl");
49 | if (ev.altKey) code.push("alt");
50 | if (ev.metaKey) code.push("meta");
51 | if (ev.shiftKey) code.push("shift");
52 | let evKey = ev.key.toLocaleLowerCase();
53 | if (ev.key === " ") {
54 | evKey = "space";
55 | }
56 | code.push(evKey);
57 |
58 | const key = code.join("+");
59 | for (let i = chain.length - 1; i >= 0; i--) {
60 | const t = chain[i];
61 | const h = t.store.get(key) || t.store.get(evKey);
62 | const tagName = ev.target.tagName;
63 | if (h && tagName !== "INPUT" && tagName !== "TEXTAREA") {
64 | if (t.isActive()) {
65 | h(ev);
66 | ev.preventDefault();
67 | return;
68 | }
69 | }
70 | }
71 | }
72 | });
73 | }
74 |
--------------------------------------------------------------------------------
/svelte/src/helpers/locate.js:
--------------------------------------------------------------------------------
1 | export function getID(node) {
2 | const id = node.getAttribute("data-id");
3 | const numId = parseInt(id);
4 | return isNaN(numId) || numId.toString() != id ? id : numId;
5 | }
6 |
--------------------------------------------------------------------------------
/svelte/src/helpers/reorder.js:
--------------------------------------------------------------------------------
1 | import { getID } from "./locate";
2 | import { locate } from "wx-lib-dom";
3 |
4 | function getOffset(node, relative, ev) {
5 | const box = node.getBoundingClientRect();
6 | const base = relative.querySelector(".wx-body").getBoundingClientRect();
7 |
8 | return {
9 | top: box.top - base.top,
10 | left: box.left - base.left,
11 | dt: box.bottom - ev.clientY,
12 | db: ev.clientY - box.top,
13 | };
14 | }
15 |
16 | const SHIFT = 5;
17 |
18 | export function reorder(node, config) {
19 | let source, clone, sid;
20 | let x, y, base, detail;
21 | let touched, touchTimer;
22 |
23 | function down(event) {
24 | x = event.clientX;
25 | y = event.clientY;
26 | base = {
27 | ...getOffset(source, node, event),
28 | y: config.getTask(sid).$y,
29 | };
30 |
31 | document.body.style.userSelect = "none";
32 | }
33 |
34 | function handleTouchstart(event) {
35 | source = locate(event);
36 | if (!source) return;
37 | sid = getID(source);
38 |
39 | touchTimer = setTimeout(() => {
40 | touched = true;
41 | if (config && config.touchStart) config.touchStart();
42 | down(event.touches[0]);
43 | }, 500);
44 |
45 | node.addEventListener("touchmove", handleTouchmove);
46 | node.addEventListener("contextmenu", handleContext);
47 | window.addEventListener("touchend", handleTouchend);
48 | }
49 |
50 | function handleContext(event) {
51 | if (touched || touchTimer) {
52 | event.preventDefault();
53 | return false;
54 | }
55 | }
56 |
57 | function handleMousedown(event) {
58 | if (event.which !== 1) return;
59 |
60 | source = locate(event);
61 | if (!source) return;
62 | sid = getID(source);
63 |
64 | node.addEventListener("mousemove", handleMousemove);
65 | window.addEventListener("mouseup", handleMouseup);
66 |
67 | down(event);
68 | }
69 |
70 | function end(full) {
71 | node.removeEventListener("mousemove", handleMousemove);
72 | node.removeEventListener("touchmove", handleTouchmove);
73 | document.body.removeEventListener("mouseup", handleMouseup);
74 | document.body.removeEventListener("touchend", handleTouchend);
75 | document.body.style.userSelect = "";
76 |
77 | if (full) {
78 | node.removeEventListener("mousedown", handleMousedown);
79 | node.removeEventListener("touchstart", handleTouchstart);
80 | }
81 | }
82 |
83 | function move(event) {
84 | const dx = event.clientX - x;
85 | const dy = event.clientY - y;
86 | if (!clone) {
87 | if (Math.abs(dx) < SHIFT && Math.abs(dy) < SHIFT) return;
88 | if (config && config.start) {
89 | if (config.start({ id: sid, e: event }) === false) return;
90 | }
91 |
92 | clone = source.cloneNode(true);
93 | clone.style.pointerEvents = "none";
94 | clone.classList.add("wx-reorder-task");
95 | clone.style.position = "absolute";
96 | clone.style.left = base.left + "px";
97 | clone.style.top = base.top + "px";
98 |
99 | source.style.visibility = "hidden";
100 | source.parentNode.insertBefore(clone, source);
101 | }
102 |
103 | if (clone) {
104 | const top = Math.round(Math.max(0, base.top + dy));
105 |
106 | if (config && config.move) {
107 | if (config.move({ id: sid, top, detail }) === false) return;
108 | }
109 |
110 | const y = config.getTask(sid).$y;
111 | //dnd may be blocked
112 | if (!base.start && base.y == y) return up();
113 |
114 | base.start = true;
115 | base.y = config.getTask(sid).$y - 4;
116 | clone.style.top = top + "px"; //task.$y - scroll
117 |
118 | const targetNode = document.elementFromPoint(
119 | event.clientX,
120 | event.clientY
121 | );
122 | const target = locate(targetNode);
123 |
124 | if (target && target !== source) {
125 | const tid = getID(target);
126 | const box = target.getBoundingClientRect();
127 | const line = box.top + box.height / 2;
128 |
129 | const after =
130 | event.clientY + base.db > line &&
131 | target.nextElementSibling !== source;
132 | const before =
133 | event.clientY - base.dt < line &&
134 | target.previousElementSibling !== source;
135 |
136 | if (detail?.after == tid || detail?.before == tid) {
137 | // avoid duplicate calls
138 | detail = null;
139 | } else if (after) {
140 | // move down
141 | detail = { id: sid, after: tid };
142 | } else if (before) {
143 | // move up
144 | detail = { id: sid, before: tid };
145 | }
146 | }
147 | }
148 | }
149 |
150 | function handleMousemove(event) {
151 | move(event);
152 | }
153 |
154 | function handleTouchmove(event) {
155 | if (touched) {
156 | event.preventDefault();
157 | move(event.touches[0]);
158 | } else if (touchTimer) {
159 | clearTimeout(touchTimer);
160 | touchTimer = null;
161 | }
162 | }
163 |
164 | function handleTouchend() {
165 | touched = null;
166 | if (touchTimer) {
167 | clearTimeout(touchTimer);
168 | touchTimer = null;
169 | }
170 | up();
171 | }
172 |
173 | function handleMouseup() {
174 | up();
175 | }
176 |
177 | function up() {
178 | if (source) {
179 | source.style.visibility = "";
180 | }
181 | if (clone) {
182 | clone.parentNode.removeChild(clone);
183 | if (config && config.end) config.end({ id: sid, top: base.top });
184 | }
185 |
186 | sid = source = clone = base = detail = null;
187 | end();
188 | }
189 |
190 | if (node.style.position !== "absolute") node.style.position = "relative";
191 |
192 | node.addEventListener("mousedown", handleMousedown);
193 | node.addEventListener("touchstart", handleTouchstart);
194 |
195 | return {
196 | destroy() {
197 | end(true);
198 | },
199 | };
200 | }
201 |
--------------------------------------------------------------------------------
/svelte/src/index.js:
--------------------------------------------------------------------------------
1 | import Gantt from "./components/Gantt.svelte";
2 | import Fullscreen from "./components/Fullscreen.svelte";
3 | import Toolbar from "./components/Toolbar.svelte";
4 | import ContextMenu from "./components/ContextMenu.svelte";
5 |
6 | import Tooltip from "./widgets/Tooltip.svelte";
7 |
8 | import Material from "./themes/Material.svelte";
9 | import Willow from "./themes/Willow.svelte";
10 | import WillowDark from "./themes/WillowDark.svelte";
11 |
12 | export {
13 | defaultEditorShape,
14 | defaultToolbarButtons,
15 | defaultMenuOptions,
16 | defaultColumns,
17 | } from "wx-gantt-store";
18 |
19 | export {
20 | Gantt,
21 | Fullscreen,
22 | ContextMenu,
23 | Toolbar,
24 | Tooltip,
25 | Material,
26 | Willow,
27 | WillowDark,
28 | };
29 |
--------------------------------------------------------------------------------
/svelte/src/themes/Material.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 | {#if children}
7 | {@render children()}
8 | {:else}
9 |
10 | {/if}
11 |
12 |
90 |
--------------------------------------------------------------------------------
/svelte/src/themes/Willow.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 | {#if children}
7 | {@render children()}
8 | {:else}
9 |
10 | {/if}
11 |
12 |
92 |
--------------------------------------------------------------------------------
/svelte/src/themes/WillowDark.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 | {#if children}
7 | {@render children()}
8 | {:else}
9 |
10 | {/if}
11 |
12 |
95 |
--------------------------------------------------------------------------------
/svelte/src/widgets/Counter.svelte:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
{label}
49 |
50 |
51 |
59 |
60 |
61 |
62 |
72 |
73 |
81 |
85 |
86 |
87 |
88 |
89 |
90 |
164 |
--------------------------------------------------------------------------------
/svelte/src/widgets/IconButton.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {#if icon} {/if}
7 |
8 |
9 |
47 |
--------------------------------------------------------------------------------
/svelte/src/widgets/Tooltip.svelte:
--------------------------------------------------------------------------------
1 |
93 |
94 |
95 |
96 |
113 |
114 |
137 |
--------------------------------------------------------------------------------
/svelte/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 |
--------------------------------------------------------------------------------
/svelte/tests/Index.svelte:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
98 |
--------------------------------------------------------------------------------
/svelte/tests/cases/LocalData.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/svelte/tests/common/Index.svelte:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
104 |
--------------------------------------------------------------------------------
/svelte/tests/common/ListRoutes.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
7 | {#each routes as route}
8 | {'"' + route + '",\n'}
9 | {/each}
10 |
12 |
13 |
18 |
--------------------------------------------------------------------------------
/svelte/tests/data.js:
--------------------------------------------------------------------------------
1 | export function getData() {
2 | const data = [];
3 | return { data };
4 | }
5 |
--------------------------------------------------------------------------------
/svelte/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Widgets
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/svelte/tests/index.js:
--------------------------------------------------------------------------------
1 | import Demos from "./Index.svelte";
2 | import { mount } from "svelte";
3 |
4 | mount(Demos, {
5 | target: document.querySelector("#wx_demo_area") || document.body,
6 | });
7 |
--------------------------------------------------------------------------------
/svelte/tests/routes.js:
--------------------------------------------------------------------------------
1 | import LocalData from "./cases/LocalData.svelte";
2 |
3 | export const links = [["/local-data", "", LocalData]];
4 |
--------------------------------------------------------------------------------
/svelte/vite.config.js:
--------------------------------------------------------------------------------
1 | import { loadEnv } from "vite";
2 | import { resolve } from "path";
3 | import { existsSync } from "fs";
4 | import { svelte } from "@sveltejs/vite-plugin-svelte";
5 | import { visualizer } from "rollup-plugin-visualizer";
6 | import { waitChanges, waitOn } from "wx-vite-tools";
7 | import conditionalCompile from "vite-plugin-conditional-compile";
8 | import pkg from "./package.json" with { type: "json" };
9 |
10 | export default async ({ mode }) => {
11 | process.env = { ...process.env, ...loadEnv(mode, process.cwd(), "WX") };
12 | const files = [];
13 |
14 | if (mode !== "production") {
15 | const paths = [
16 | resolve(__dirname, "../store/dist/index.js"),
17 | resolve(__dirname, "../provider/dist/index.js"),
18 | ];
19 |
20 | paths.forEach(path => {
21 | if (existsSync(path)) {
22 | files.push(path);
23 | }
24 | });
25 | }
26 |
27 | const plugins = [];
28 |
29 | if (files.length) plugins.push(waitChanges({ files }));
30 | if (mode !== "development") plugins.push(conditionalCompile());
31 | plugins.push(svelte({}));
32 |
33 | const name = pkg.productTag;
34 |
35 | let build,
36 | publicDir = resolve(__dirname, "public"),
37 | server = {},
38 | base = "";
39 |
40 | if (mode === "test") {
41 | build = {
42 | rollupOptions: {
43 | input: { tests: resolve(__dirname, "tests/index.html") },
44 | },
45 | };
46 | server.port = 5100;
47 | } else {
48 | build = {
49 | rollupOptions: {
50 | input: { index: resolve(__dirname, "index.html") },
51 | },
52 | };
53 | }
54 |
55 | if (process.env.WX_BUILD_STATS) {
56 | build = {
57 | lib: {
58 | entry: resolve(__dirname, "src/index.js"),
59 | name,
60 | formats: ["es"],
61 | fileName: format => `${name}.${format}.js`,
62 | },
63 | outDir: "./dist",
64 | sourcemap: true,
65 | minify: true,
66 | target: "esnext",
67 | };
68 | publicDir = false;
69 | plugins.push(visualizer({ filename: "dist/stats.html" }));
70 | }
71 |
72 | if (files.length) await waitOn({ files });
73 |
74 | return {
75 | base,
76 | build,
77 | publicDir,
78 | resolve: { dedupe: ["svelte"] },
79 | plugins,
80 | server,
81 | watch: {
82 | persistent: true,
83 | include: ["src/**/*.ts", "src/**/*.js"],
84 | },
85 | };
86 | };
87 |
--------------------------------------------------------------------------------
/svelte/whatsnew.md:
--------------------------------------------------------------------------------
1 | ## 2.1.1
2 |
3 | ### Fixes
4 |
5 | - Regression with table and chart alignment
6 | - Regression with fullscreen functionality
7 | - Right and center alignment does not apply to header cells
8 |
9 | ## 2.1.0
10 |
11 | ### Updates
12 |
13 | - Ability to use custom HTML in column cells
14 | - Optimizing inner zoom and scale logic
15 |
16 | ### Fixes
17 |
18 | - Impossible to use numeric id for Toolbar and Menu options
19 |
20 | ## 2.0.1
21 |
22 | ### New features
23 |
24 | - Svelte 5 support
25 |
26 | ## 1.2.0
27 |
28 | - Released under GPLv3
29 |
30 | ## 1.1.0
31 |
32 | ### New features
33 |
34 | - Summary tasks
35 | - Sorting Grid columns
36 | - Batch mode for DataProvider to handle mass operations
37 | - `sort-tasks` action is added
38 | - POST /{batchURL} route is added
39 | - `columns` property is extended with the `sort` parameter
40 | - `minCellWidth` and `maxCellWidth` settings are added to the `zoom` property
41 | - `update-task` and `copy-task` actions are extended with the `eventSource` parameter
42 | - `source` parameter is added to the "move-task" and "delete-task" actions
43 | - RestDataProvider config has the `batchUrl` property
44 | - New parameters (`source` and `lazy`) added to the "copy-task" action
45 |
46 | ### Updates
47 |
48 | - Common minCellwidth and maxCellWidth for zoom levels
49 | - Skipping meaningless actions in Context Menu and Toolbar
50 |
51 | ### Fixes
52 |
53 | - Scroll in Grid is not smooth
54 | - Impossible to resize chart bars on the left more than for one cell
55 | - When a branch is removed, only links of the parent task are removed
56 | - Unstable move down behaviour: tasks are inserted in wrong positions
57 | - Resizing columns: horizontal scrollbar does not appear
58 | - Text of tasks is higher than a dragged bar during reordering
59 | - Last task is misplaced after reordering when there are few tasks
60 | - Reordering of tasks with child tasks is broken
61 | - Impossible to define cellWidth if default zoom is enabled
62 | - Data is removed from task object after the "update-task" operation
63 | - Outdenting does not work for 3rd-level tasks
64 | - Auto scale is calculated incorrectly if the last task is a milestone
65 | - Parent task is not always opened after adding a new task
66 | - Task start date is not set according to the top-level target task
67 | - Zooming in and out between levels does not work correctly
68 |
69 | ## 1.1
70 |
71 | ### New features
72 |
73 | - Summary tasks
74 | - Sorting Grid columns
75 | - Batch mode for DataProvider to handle mass operations
76 | - New "sort-tasks" action is added
77 | - New POST /{batchURL} route is added
78 | - The `columns` property is extended with the `sort` parameter
79 | - The `minCellWidth` and `maxCellWidth` settings are added to the `zoom` property
80 | - The "update-task" and "copy-task" actions are extended with the `eventSource` parameter
81 | - The `source` parameter is added to the "move-task" and "delete-task" actions
82 | - RestDataProvider config has the `batchUrl` property
83 | - New parameters (`source` and `lazy`) added to the "copy-task" action
84 |
85 | ### Updates
86 |
87 | - Common minCellwidth and maxCellWidth for zoom levels
88 | - Skipping meaningless actions in Context Menu and Toolbar
89 |
90 | ### Fixes
91 |
92 | - Scroll in Grid is not smooth
93 | - Impossible to resize chart bars on the left more than for one cell
94 | - When a branch is removed, only links of the parent task are removed
95 | - Unstable move down behaviour: tasks are inserted in wrong positions
96 | - Resizing columns: horizontal scrollbar does not appear
97 | - Text of tasks is higher than a dragged bar during reordering
98 | - Last task is misplaced after reordering when there are few tasks
99 | - Reordering of tasks with child tasks is broken
100 | - Impossible to define cellWidth if default zoom is enabled
101 | - Data is removed from task object after the "update-task" operation
102 | - Outdenting does not work for 3rd-level tasks
103 | - Auto scale is calculated incorrectly if the last task is a milestone
104 | - Parent task is not always opened after adding a new task
105 | - Task start date is not set according to the top-level target task
106 | - Zooming in and out between levels does not work correctly
107 |
108 | ## 1.0
109 |
110 | ### Initial features
111 |
112 | - Fast behavior and clear API
113 | - Configurable Grid columns
114 | - Configurable Chart scales and cell sizes
115 | - Task types:"project", "task", "milestone" and custom
116 | - Baselines
117 | - Holiday and custom markers in Chart area
118 | - Configurable Editor panel
119 | - Configurable Context Menu and Toolbar
120 | - Tooltips for tasks in Chart area
121 | - Readonly mode
122 | - Fullscreen mode
123 | - Wouse-wheel zooming in Chart area
124 | - Responsive behaviour of Grid area
125 | - Localization of labels and dates
126 | - Ready-made DataProvider to intergate with server
127 |
--------------------------------------------------------------------------------
/vitest.workspace.js:
--------------------------------------------------------------------------------
1 | import { defineWorkspace } from "vitest/config";
2 |
3 | export default defineWorkspace([
4 | "./site/vite.config.js",
5 | "./store/vite.config.js",
6 | "./svelte/vite.config.js",
7 | "./provider/vite.config.js",
8 | ]);
9 |
--------------------------------------------------------------------------------