@operato/gantt 8.0.0-alpha.10 → 8.0.0-alpha.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@operato/gantt",
3
3
  "description": "Webcomponents for gantt chart following open-wc recommendations",
4
4
  "author": "heartyoh@hatiolab.com",
5
- "version": "8.0.0-alpha.10",
5
+ "version": "8.0.0-alpha.27",
6
6
  "main": "dist/src/index.js",
7
7
  "module": "dist/src/index.js",
8
8
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@operato/i18n": "^8.0.0-alpha.0",
46
- "@operato/styles": "^8.0.0-alpha.4",
46
+ "@operato/styles": "^8.0.0-alpha.27",
47
47
  "@operato/utils": "^8.0.0-alpha.0",
48
48
  "d3": "^7.8.5",
49
49
  "d3-time-format": "^4.1.0",
@@ -85,5 +85,5 @@
85
85
  "prettier --write"
86
86
  ]
87
87
  },
88
- "gitHead": "f937b76c9831c46e26acde8e94cc71514068a961"
88
+ "gitHead": "7f16dfa02491950f7a8782cb305605925a9e0be4"
89
89
  }
@@ -3,18 +3,22 @@ import * as d3 from 'd3'
3
3
  import { TimeScale, Task, TaskTag } from './types'
4
4
  import { OxTaskTooltip } from './ox-task-tooltip'
5
5
  import { drawTimeScaleHeader, getScale, getChartWidth } from './draw-time-scale-header'
6
+ import { levelDown, levelUp } from './utils'
6
7
 
7
8
  export class GanttChartController implements ReactiveController {
8
- private host: ReactiveControllerHost & LitElement
9
+ private host: (ReactiveControllerHost & LitElement) | null
9
10
  private fromTime: Date
10
11
  private toTime: Date
11
12
  private totalDays: number
12
13
  private _timeScale?: d3.ScaleTime<number, number, never>
13
14
 
14
- private touchStartX?: number
15
- private touchStartY?: number
16
- private scrollStartX?: number
17
- private scrollStartY?: number
15
+ private touchStartX: number | null = null
16
+ private touchStartY: number | null = null
17
+ private scrollStartX: number | null = null
18
+ private scrollStartY: number | null = null
19
+ private lastTouchDistance?: number | null
20
+ private draggingThreshold: number = 20
21
+ private dragging: boolean = false
18
22
 
19
23
  private colorProvider?: (task: Task) => string
20
24
 
@@ -34,6 +38,10 @@ export class GanttChartController implements ReactiveController {
34
38
  this.host.addController(this)
35
39
  }
36
40
 
41
+ dispose() {
42
+ this.host = null
43
+ }
44
+
37
45
  hostConnected() {
38
46
  this.updateTimeScale()
39
47
  }
@@ -57,12 +65,12 @@ export class GanttChartController implements ReactiveController {
57
65
  this.toTime = new Date(toDate)
58
66
  this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
59
67
  this._timeScale = undefined
60
- this.host.requestUpdate()
68
+ this.host?.requestUpdate()
61
69
  }
62
70
 
63
71
  setTasks(tasks: Task[]) {
64
72
  this.tasks = tasks
65
- this.host.requestUpdate()
73
+ this.host?.requestUpdate()
66
74
  }
67
75
 
68
76
  updateTimeScale() {
@@ -93,7 +101,7 @@ export class GanttChartController implements ReactiveController {
93
101
  .append('marker')
94
102
  .attr('id', 'arrow')
95
103
  .attr('viewBox', '0 0 10 10')
96
- .attr('refX', 10) // Adjusted refX to move the arrow tip closer to the line's end
104
+ .attr('refX', 10)
97
105
  .attr('refY', 5)
98
106
  .attr('markerWidth', 6)
99
107
  .attr('markerHeight', 6)
@@ -150,14 +158,20 @@ export class GanttChartController implements ReactiveController {
150
158
  const taskWidth = endX - startX + this.baseWidth / this.getScale()
151
159
  const taskY = index * 30 + 7.5
152
160
 
153
- const showTooltip = (event: MouseEvent) => {
161
+ const showTooltip = (event: MouseEvent | TouchEvent) => {
162
+ tooltip.hide()
163
+
164
+ const touch = 'touches' in event
165
+ const x = touch ? event.touches[0].clientX : event.clientX
166
+ const y = touch ? event.touches[0].clientY : event.clientY
167
+
154
168
  tooltip.title = task.title
155
169
  tooltip.startDate = startDate.toISOString().split('T')[0]
156
170
  tooltip.endDate = endDate.toISOString().split('T')[0]
157
171
  tooltip.progress = task.progress ?? 0
158
172
  tooltip.type = task.type
159
173
 
160
- tooltip.show(event.clientX, event.clientY)
174
+ tooltip.show(x, y)
161
175
  }
162
176
 
163
177
  const hideTooltip = () => {
@@ -192,8 +206,21 @@ export class GanttChartController implements ReactiveController {
192
206
  bubbles: true,
193
207
  composed: true
194
208
  })
195
- this.host.dispatchEvent(customEvent)
196
- this.host.requestUpdate()
209
+ this.host?.dispatchEvent(customEvent)
210
+ this.host?.requestUpdate()
211
+ })
212
+ .on('touchstart', e => {
213
+ this.selectedTaskId = task.id
214
+ const customEvent = new CustomEvent('task-clicked', {
215
+ detail: { taskId: task.id },
216
+ bubbles: true,
217
+ composed: true
218
+ })
219
+
220
+ showTooltip(e)
221
+
222
+ this.host?.dispatchEvent(customEvent)
223
+ this.host?.requestUpdate()
197
224
  })
198
225
  .on('mouseover', showTooltip)
199
226
  .on('mouseout', hideTooltip)
@@ -216,8 +243,21 @@ export class GanttChartController implements ReactiveController {
216
243
  bubbles: true,
217
244
  composed: true
218
245
  })
219
- this.host.dispatchEvent(customEvent)
220
- this.host.requestUpdate()
246
+ this.host?.dispatchEvent(customEvent)
247
+ this.host?.requestUpdate()
248
+ })
249
+ .on('touchstart', e => {
250
+ this.selectedTaskId = task.id
251
+ const customEvent = new CustomEvent('task-clicked', {
252
+ detail: { taskId: task.id },
253
+ bubbles: true,
254
+ composed: true
255
+ })
256
+
257
+ showTooltip(e)
258
+
259
+ this.host?.dispatchEvent(customEvent)
260
+ this.host?.requestUpdate()
221
261
  })
222
262
  .on('mouseover', showTooltip)
223
263
  .on('mouseout', hideTooltip)
@@ -256,7 +296,7 @@ export class GanttChartController implements ReactiveController {
256
296
  .attr('stroke', 'var(--md-sys-color-on-primary-container, black)')
257
297
  .attr('fill', 'none')
258
298
  .attr('stroke-width', 1)
259
- .attr('marker-end', 'url(#arrow)') // Add arrow marker
299
+ .attr('marker-end', 'url(#arrow)')
260
300
  }
261
301
  })
262
302
  }
@@ -299,38 +339,132 @@ export class GanttChartController implements ReactiveController {
299
339
  composed: true
300
340
  })
301
341
 
302
- this.host.dispatchEvent(customEvent)
342
+ this.host?.dispatchEvent(customEvent)
303
343
  }
304
344
 
305
- handleTouchStartForBody(event: TouchEvent, container: HTMLDivElement) {
345
+ handleTouchStartForBody(event: MouseEvent | TouchEvent, container: HTMLDivElement) {
346
+ event.preventDefault()
306
347
  event.stopPropagation()
307
348
 
308
- this.touchStartX = event.touches[0].clientX
309
- this.touchStartY = event.touches[0].clientY
349
+ const touch = 'touches' in event
350
+ const x = touch ? event.touches[0].clientX : event.clientX
351
+ const y = touch ? event.touches[0].clientY : event.clientY
310
352
 
311
- this.scrollStartX = container.scrollLeft
312
- this.scrollStartY = container.scrollTop
353
+ this.dragging = false
354
+
355
+ if (touch && event.touches.length === 2) {
356
+ this.lastTouchDistance = this.getTouchDistance(event.touches)
357
+ } else {
358
+ this.touchStartX = x
359
+ this.touchStartY = y
360
+
361
+ this.scrollStartX = container.scrollLeft
362
+ this.scrollStartY = container.scrollTop
363
+ }
313
364
  }
314
365
 
315
- handleTouchMoveForBody(event: TouchEvent, container: HTMLDivElement) {
316
- const touchCurrentX = event.touches[0].clientX
317
- const touchCurrentY = event.touches[0].clientY
366
+ handleTouchMoveForBody(event: MouseEvent | TouchEvent, container: HTMLDivElement) {
367
+ event.preventDefault()
368
+ event.stopPropagation()
318
369
 
319
- const deltaX = this.touchStartX! - touchCurrentX
320
- const deltaY = this.touchStartY! - touchCurrentY
370
+ const touch = 'touches' in event
371
+ const x = touch ? event.touches[0].clientX : event.clientX
372
+ const y = touch ? event.touches[0].clientY : event.clientY
373
+
374
+ if (touch && event.touches.length > 1 && this.lastTouchDistance !== null) {
375
+ const currentDistance = this.getTouchDistance(event.touches)
376
+
377
+ const zoomSensitivity = 0.3
378
+ let zoomFactor = currentDistance / this.lastTouchDistance!
379
+ zoomFactor = 1 + (zoomFactor - 1) * zoomSensitivity
380
+
381
+ this.baseWidth *= zoomFactor
382
+
383
+ if (this.baseWidth > 40) {
384
+ if (this.level === 'week-day') {
385
+ this.baseWidth = 40
386
+ } else {
387
+ this.level = levelDown(this.level)
388
+ this.baseWidth = 30
389
+ }
390
+ } else if (this.baseWidth < 30) {
391
+ if (this.level === 'year-quarter') {
392
+ this.baseWidth = 30
393
+ }
394
+ }
321
395
 
322
- container.scrollLeft = this.scrollStartX! + deltaX
323
- container.scrollTop = this.scrollStartY! + deltaY
396
+ if (this.baseWidth < 20) {
397
+ this.level = levelUp(this.level)
398
+ this.baseWidth = 30
399
+ }
324
400
 
325
- const customEvent = new CustomEvent('scroll', {
326
- detail: {
327
- scrollLeft: container.scrollLeft,
328
- scrollTop: container.scrollTop
329
- },
330
- bubbles: true,
331
- composed: true
332
- })
401
+ this.lastTouchDistance = currentDistance
402
+ this.updateTimeScale()
403
+ this.host?.requestUpdate()
404
+
405
+ const customEvent = new CustomEvent('timescale-changed', {
406
+ detail: {
407
+ baseWidth: this.baseWidth,
408
+ level: this.level
409
+ },
410
+ bubbles: true,
411
+ composed: true
412
+ })
413
+ this.host?.dispatchEvent(customEvent)
414
+ } else if (this.touchStartX !== null) {
415
+ const dx = this.touchStartX! - x
416
+ const dy = this.touchStartY! - y
417
+
418
+ if (!this.dragging) {
419
+ if (Math.sqrt(dx * dx + dy * dy) > this.draggingThreshold) {
420
+ this.dragging = true
421
+ } else {
422
+ return
423
+ }
424
+ }
425
+
426
+ container.scrollLeft = this.scrollStartX! + dx
427
+ container.scrollTop = this.scrollStartY! + dy
428
+
429
+ const customEvent = new CustomEvent('scroll', {
430
+ detail: {
431
+ scrollLeft: container.scrollLeft,
432
+ scrollTop: container.scrollTop
433
+ },
434
+ bubbles: true,
435
+ composed: true
436
+ })
437
+
438
+ this.host?.dispatchEvent(customEvent)
439
+ }
440
+ }
441
+
442
+ handleTouchEndForBody(event: MouseEvent | TouchEvent) {
443
+ event.preventDefault()
444
+ event.stopPropagation()
445
+
446
+ if ('touches' in event && event.touches.length > 1) {
447
+ this.lastTouchDistance = null
448
+ } else if (this.touchStartX !== null) {
449
+ if (!this.dragging) {
450
+ this.host?.dispatchEvent(
451
+ new CustomEvent('date-range-cleared', {
452
+ bubbles: true,
453
+ composed: true
454
+ })
455
+ )
456
+ }
457
+
458
+ this.touchStartX = null
459
+ this.touchStartY = null
460
+ this.scrollStartX = null
461
+ this.scrollStartY = null
462
+ }
463
+ }
333
464
 
334
- this.host.dispatchEvent(customEvent)
465
+ getTouchDistance(touches: TouchList): number {
466
+ const dx = touches[0].clientX - touches[1].clientX
467
+ const dy = touches[0].clientY - touches[1].clientY
468
+ return Math.sqrt(dx * dx + dy * dy)
335
469
  }
336
470
  }
@@ -23,15 +23,20 @@ export class OxGanttChart extends LitElement {
23
23
  display: flex;
24
24
  overflow: hidden;
25
25
  position: relative;
26
+ user-select: none;
26
27
  }
28
+
27
29
  div[chart-container] {
28
30
  display: block;
29
31
  overflow-x: auto;
30
32
  overflow-y: hidden;
33
+ overscroll-behavior: none;
31
34
  }
35
+
32
36
  svg {
33
37
  min-width: 100%;
34
38
  }
39
+
35
40
  svg[header] {
36
41
  display: block;
37
42
  min-width: 100%;
@@ -44,27 +49,27 @@ export class OxGanttChart extends LitElement {
44
49
  top: 0;
45
50
  z-index: 1;
46
51
  }
47
- .scroll-buttons {
48
- position: absolute;
49
- top: 44px; /* 헤더 바로 아래 위치 */
50
- right: 4px;
51
- display: flex;
52
- align-items: center;
53
- gap: 2px;
54
- }
52
+
55
53
  .scroll-button {
56
- width: 10px;
57
- height: 10px;
54
+ position: absolute;
55
+ top: 46px; /* 헤더 바로 아래 위치 */
56
+ width: 14px;
57
+ height: 14px;
58
58
  background-color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
59
+ opacity: 0.5;
59
60
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
60
61
  cursor: pointer;
61
62
  transform: rotate(90deg);
62
63
  }
64
+
63
65
  .scroll-button.left {
66
+ left: 6px;
64
67
  transform: rotate(-90deg);
65
68
  }
69
+
66
70
  .scroll-button.right {
67
71
  transform: rotate(90deg);
72
+ right: 6px;
68
73
  }
69
74
  `
70
75
  ]
@@ -95,6 +100,14 @@ export class OxGanttChart extends LitElement {
95
100
 
96
101
  protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
97
102
  this.initHeaderGestureHandlers()
103
+
104
+ this.addEventListener('date-range-cleared', e => this.clearSelection())
105
+ }
106
+
107
+ disconnectedCallback(): void {
108
+ super.disconnectedCallback()
109
+
110
+ this.ganttChartController.dispose()
98
111
  }
99
112
 
100
113
  updated(changedProperties: PropertyValues<this>) {
@@ -133,8 +146,12 @@ export class OxGanttChart extends LitElement {
133
146
  <div
134
147
  chart-container
135
148
  @wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}
149
+ @mousedown=${(e: MouseEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
150
+ @mousemove=${(e: MouseEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
151
+ @mouseup=${(e: MouseEvent) => this.ganttChartController.handleTouchEndForBody(e)}
136
152
  @touchstart=${(e: TouchEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
137
153
  @touchmove=${(e: TouchEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
154
+ @touchend=${(e: TouchEvent) => this.ganttChartController.handleTouchEndForBody(e)}
138
155
  >
139
156
  <svg
140
157
  header
@@ -142,10 +159,8 @@ export class OxGanttChart extends LitElement {
142
159
  @wheel=${this.handleWheelForHeader.bind(this)}
143
160
  ></svg>
144
161
  <svg body></svg>
145
- <div class="scroll-buttons">
146
- <div class="scroll-button left" @click=${this.scrollToStart}></div>
147
- <div class="scroll-button right" @click=${this.scrollToEnd}></div>
148
- </div>
162
+ <div class="scroll-button left" @click=${this.scrollToStart} @touchstart=${this.scrollToStart}></div>
163
+ <div class="scroll-button right" @click=${this.scrollToEnd} @touchstart=${this.scrollToEnd}></div>
149
164
  <ox-task-tooltip></ox-task-tooltip>
150
165
  </div>
151
166
  `
@@ -266,60 +281,22 @@ export class OxGanttChart extends LitElement {
266
281
 
267
282
  let isDragging = false
268
283
  let startX: number | null = null
269
- let lastTouchDistance: number | null = null
270
284
 
271
285
  const handleTouchStart = (event: TouchEvent) => {
272
286
  event.preventDefault()
287
+ event.stopPropagation()
273
288
 
274
- if (event.touches.length === 2) {
275
- lastTouchDistance = getTouchDistance(event.touches)
276
- } else {
277
- isDragging = true
278
- const touch = event.touches[0]
279
- const containerOffsetX = header.node()!.getBoundingClientRect().left
280
- startX = touch.clientX - containerOffsetX
281
- }
289
+ isDragging = true
290
+ const touch = event.touches[0]
291
+ const containerOffsetX = header.node()!.getBoundingClientRect().left
292
+ startX = touch.clientX - containerOffsetX
282
293
  }
283
294
 
284
295
  const handleTouchMove = (event: TouchEvent) => {
285
296
  event.preventDefault()
297
+ event.stopPropagation()
286
298
 
287
- if (event.touches.length === 2 && lastTouchDistance !== null) {
288
- const currentDistance = getTouchDistance(event.touches)
289
- const zoomFactor = currentDistance / lastTouchDistance
290
- this.ganttChartController.baseWidth *= zoomFactor
291
-
292
- if (this.ganttChartController.baseWidth > 60) {
293
- if (this.ganttChartController.level == 'year-quarter') {
294
- this.ganttChartController.baseWidth = 60
295
- } else {
296
- this.ganttChartController.level = levelUp(this.ganttChartController.level)
297
- this.ganttChartController.baseWidth = 30
298
- }
299
- } else if (this.ganttChartController.baseWidth < 30) {
300
- if (this.ganttChartController.level == 'week-day') {
301
- this.ganttChartController.baseWidth = 30
302
- } else {
303
- this.ganttChartController.level = levelDown(this.ganttChartController.level)
304
- this.ganttChartController.baseWidth = 60
305
- }
306
- }
307
-
308
- lastTouchDistance = currentDistance
309
- this.ganttChartController.updateTimeScale()
310
- this.requestUpdate()
311
-
312
- // Dispatch custom event to sync with ox-resource-chart
313
- const customEvent = new CustomEvent('timescale-changed', {
314
- detail: {
315
- baseWidth: this.ganttChartController.baseWidth,
316
- level: this.ganttChartController.level
317
- },
318
- bubbles: true,
319
- composed: true
320
- })
321
- this.dispatchEvent(customEvent)
322
- } else if (isDragging && startX !== null) {
299
+ if (isDragging && startX !== null) {
323
300
  const touch = event.touches[0]
324
301
  const containerOffsetX = header.node()!.getBoundingClientRect().left
325
302
  const currentX = touch.clientX - containerOffsetX
@@ -329,10 +306,7 @@ export class OxGanttChart extends LitElement {
329
306
 
330
307
  const handleTouchEnd = (event: TouchEvent) => {
331
308
  event.preventDefault()
332
-
333
- if (event.touches.length < 2) {
334
- lastTouchDistance = null
335
- }
309
+ event.stopPropagation()
336
310
 
337
311
  if (!isDragging || startX === null) {
338
312
  const touch = event.changedTouches[0]
@@ -351,12 +325,6 @@ export class OxGanttChart extends LitElement {
351
325
  startX = null
352
326
  }
353
327
 
354
- const getTouchDistance = (touches: TouchList): number => {
355
- const dx = touches[0].clientX - touches[1].clientX
356
- const dy = touches[0].clientY - touches[1].clientY
357
- return Math.sqrt(dx * dx + dy * dy)
358
- }
359
-
360
328
  const handleMouseDown = (event: MouseEvent) => {
361
329
  isDragging = true
362
330
  const containerOffsetX = header.node()!.getBoundingClientRect().left
@@ -515,4 +483,13 @@ export class OxGanttChart extends LitElement {
515
483
 
516
484
  header.node()?.dispatchEvent(customEvent)
517
485
  }
486
+
487
+ clearSelection() {
488
+ const header = d3.select(this.svgHeader)
489
+ const body = d3.select(this.svgBody)
490
+
491
+ // 선택한 영역을 나타내는 모든 'selection-rect' 요소를 제거합니다.
492
+ header.selectAll('.selection-rect').remove()
493
+ body.selectAll('.selection-rect').remove()
494
+ }
518
495
  }