@operato/gantt 8.0.0-alpha.33 → 8.0.0-alpha.36

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.
@@ -4,6 +4,7 @@ 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
6
  import { levelDown, levelUp } from './utils'
7
+ import { GestureEventPinch, GestureEventDoubleTap, GestureEventDrag } from '@operato/utils/gesture-mixin.js'
7
8
 
8
9
  export class GanttChartController implements ReactiveController {
9
10
  private host: (ReactiveControllerHost & LitElement) | null
@@ -12,14 +13,6 @@ export class GanttChartController implements ReactiveController {
12
13
  private totalDays: number
13
14
  private _timeScale?: d3.ScaleTime<number, number, never>
14
15
 
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
22
-
23
16
  private colorProvider?: (task: Task) => string
24
17
 
25
18
  public baseWidth: number = 30
@@ -199,17 +192,7 @@ export class GanttChartController implements ReactiveController {
199
192
  .attr('stroke-width', 1)
200
193
 
201
194
  taskBar
202
- .on('click', () => {
203
- this.selectedTaskId = task.id
204
- const customEvent = new CustomEvent('task-clicked', {
205
- detail: { taskId: task.id },
206
- bubbles: true,
207
- composed: true
208
- })
209
- this.host?.dispatchEvent(customEvent)
210
- this.host?.requestUpdate()
211
- })
212
- .on('touchstart', e => {
195
+ .on('click', e => {
213
196
  this.selectedTaskId = task.id
214
197
  const customEvent = new CustomEvent('task-clicked', {
215
198
  detail: { taskId: task.id },
@@ -236,17 +219,7 @@ export class GanttChartController implements ReactiveController {
236
219
  .attr('fill', 'lightgreen')
237
220
 
238
221
  progressBar
239
- .on('click', () => {
240
- this.selectedTaskId = task.id
241
- const customEvent = new CustomEvent('task-clicked', {
242
- detail: { taskId: task.id },
243
- bubbles: true,
244
- composed: true
245
- })
246
- this.host?.dispatchEvent(customEvent)
247
- this.host?.requestUpdate()
248
- })
249
- .on('touchstart', e => {
222
+ .on('click', e => {
250
223
  this.selectedTaskId = task.id
251
224
  const customEvent = new CustomEvent('task-clicked', {
252
225
  detail: { taskId: task.id },
@@ -317,6 +290,8 @@ export class GanttChartController implements ReactiveController {
317
290
  event.preventDefault()
318
291
  event.stopPropagation()
319
292
 
293
+ const { offsetHeight } = container
294
+
320
295
  const deltaModeFactor =
321
296
  event.deltaMode === WheelEvent.DOM_DELTA_LINE
322
297
  ? 16
@@ -328,7 +303,8 @@ export class GanttChartController implements ReactiveController {
328
303
 
329
304
  // 가로 스크롤 및 세로 스크롤 모두 처리
330
305
  container.scrollLeft += deltaX
331
- container.scrollTop += deltaY
306
+ // container.scrollTop += deltaY
307
+ container.scrollTop = Math.min(this.tasks.length * 30 - (offsetHeight - 40), container.scrollTop + deltaY)
332
308
 
333
309
  const customEvent = new CustomEvent('scroll', {
334
310
  detail: {
@@ -342,129 +318,85 @@ export class GanttChartController implements ReactiveController {
342
318
  this.host?.dispatchEvent(customEvent)
343
319
  }
344
320
 
345
- handleTouchStartForBody(event: MouseEvent | TouchEvent, container: HTMLDivElement) {
346
- event.preventDefault()
347
- event.stopPropagation()
321
+ handlePinchForBody = (event: GestureEventPinch) => {
322
+ const { scale } = event.detail
348
323
 
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
324
+ this.baseWidth *= scale
352
325
 
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
326
+ if (this.baseWidth > 40) {
327
+ if (this.level === 'week-day') {
328
+ this.baseWidth = 40
329
+ } else {
330
+ this.level = levelDown(this.level)
331
+ this.baseWidth = 30
332
+ }
333
+ } else if (this.baseWidth < 30) {
334
+ if (this.level === 'year-quarter') {
335
+ this.baseWidth = 30
336
+ }
337
+ }
360
338
 
361
- this.scrollStartX = container.scrollLeft
362
- this.scrollStartY = container.scrollTop
339
+ if (this.baseWidth < 20) {
340
+ this.level = levelUp(this.level)
341
+ this.baseWidth = 30
363
342
  }
364
- }
365
343
 
366
- handleTouchMoveForBody(event: MouseEvent | TouchEvent, container: HTMLDivElement) {
367
- event.preventDefault()
368
- event.stopPropagation()
344
+ this.updateTimeScale()
345
+ this.host?.requestUpdate()
369
346
 
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
- }
347
+ const customEvent = new CustomEvent('timescale-changed', {
348
+ detail: {
349
+ baseWidth: this.baseWidth,
350
+ level: this.level
351
+ },
352
+ bubbles: true,
353
+ composed: true
354
+ })
395
355
 
396
- if (this.baseWidth < 20) {
397
- this.level = levelUp(this.level)
398
- this.baseWidth = 30
399
- }
356
+ this.host?.dispatchEvent(customEvent)
357
+ }
400
358
 
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
- }
359
+ handleDoubleTapForBody = (event: GestureEventDoubleTap) => {
360
+ const container = (this.host as any).container
425
361
 
426
- container.scrollLeft = this.scrollStartX! + dx
427
- container.scrollTop = this.scrollStartY! + dy
362
+ container.scrollLeft = 0
363
+ container.scrollTop = 0
428
364
 
429
- const customEvent = new CustomEvent('scroll', {
430
- detail: {
431
- scrollLeft: container.scrollLeft,
432
- scrollTop: container.scrollTop
433
- },
434
- bubbles: true,
435
- composed: true
436
- })
365
+ this.baseWidth = 30
366
+ this.level = 'week-day'
437
367
 
438
- this.host?.dispatchEvent(customEvent)
439
- }
368
+ this.updateTimeScale()
369
+ this.host?.requestUpdate()
370
+
371
+ const customEvent = new CustomEvent('timescale-changed', {
372
+ detail: {
373
+ baseWidth: this.baseWidth,
374
+ level: this.level
375
+ },
376
+ bubbles: true,
377
+ composed: true
378
+ })
379
+
380
+ this.host?.dispatchEvent(customEvent)
440
381
  }
441
382
 
442
- handleTouchEndForBody(event: MouseEvent | TouchEvent) {
443
- event.preventDefault()
444
- event.stopPropagation()
383
+ handleDragForBody = (event: GestureEventDrag) => {
384
+ const { deltaX: dx, deltaY: dy } = event.detail
385
+ const container = (this.host as any).container
386
+ const { offsetHeight } = container
445
387
 
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
- }
388
+ container.scrollLeft -= dx
389
+ container.scrollTop = Math.min(this.tasks.length * 30 - (offsetHeight - 40), container.scrollTop - dy)
457
390
 
458
- this.touchStartX = null
459
- this.touchStartY = null
460
- this.scrollStartX = null
461
- this.scrollStartY = null
462
- }
463
- }
391
+ const customEvent = new CustomEvent('scroll', {
392
+ detail: {
393
+ scrollLeft: container.scrollLeft,
394
+ scrollTop: container.scrollTop
395
+ },
396
+ bubbles: true,
397
+ composed: true
398
+ })
464
399
 
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)
400
+ this.host?.dispatchEvent(customEvent)
469
401
  }
470
402
  }
@@ -13,8 +13,10 @@ import { Task, TimeScale } from './types.js'
13
13
  import { levelDown, levelUp } from './utils'
14
14
  import { OxTaskTooltip } from './ox-task-tooltip.js'
15
15
 
16
+ import { gesture } from '@operato/utils/gesture-mixin.js'
17
+
16
18
  @customElement('ox-gantt-chart')
17
- export class OxGanttChart extends LitElement {
19
+ export class OxGanttChart extends gesture(LitElement) {
18
20
  static styles = [
19
21
  MDTypeScaleStyles,
20
22
  ScrollbarStyles,
@@ -31,6 +33,7 @@ export class OxGanttChart extends LitElement {
31
33
  overflow-x: auto;
32
34
  overflow-y: hidden;
33
35
  overscroll-behavior: none;
36
+ touch-action: none;
34
37
  }
35
38
 
36
39
  svg {
@@ -91,12 +94,7 @@ export class OxGanttChart extends LitElement {
91
94
  @query('[chart-container]') container!: HTMLDivElement
92
95
  @query('ox-task-tooltip') tooltip!: OxTaskTooltip
93
96
 
94
- private ganttChartController: GanttChartController
95
-
96
- constructor() {
97
- super()
98
- this.ganttChartController = new GanttChartController(this, this.fromDate, this.toDate, this.axisScale)
99
- }
97
+ private ganttChartController!: GanttChartController
100
98
 
101
99
  protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
102
100
  this.initHeaderGestureHandlers()
@@ -104,9 +102,22 @@ export class OxGanttChart extends LitElement {
104
102
  this.addEventListener('date-range-cleared', e => this.clearSelection())
105
103
  }
106
104
 
105
+ connectedCallback(): void {
106
+ super.connectedCallback()
107
+ this.ganttChartController = new GanttChartController(this, this.fromDate, this.toDate, this.axisScale)
108
+
109
+ this.addEventListener('pinch', this.ganttChartController.handlePinchForBody as EventListener)
110
+ this.addEventListener('drag', this.ganttChartController.handleDragForBody as EventListener)
111
+ this.addEventListener('doubletap', this.ganttChartController.handleDoubleTapForBody as EventListener)
112
+ }
113
+
107
114
  disconnectedCallback(): void {
108
115
  super.disconnectedCallback()
109
116
 
117
+ this.removeEventListener('pinch', this.ganttChartController.handlePinchForBody as EventListener)
118
+ this.removeEventListener('drag', this.ganttChartController.handleDragForBody as EventListener)
119
+ this.removeEventListener('doubletap', this.ganttChartController.handleDoubleTapForBody as EventListener)
120
+
110
121
  this.ganttChartController.dispose()
111
122
  }
112
123
 
@@ -116,8 +127,9 @@ export class OxGanttChart extends LitElement {
116
127
  }
117
128
 
118
129
  if (changedProperties.has('tasks')) {
119
- this.ganttChartController.setTasks(this.flattenTasks(this.tasks))
120
- this.svgBody.style.height = Math.max(this.tasks.length * 30, 2000) + 'px'
130
+ const flattenTasks = this.flattenTasks(this.tasks)
131
+ this.ganttChartController.setTasks(flattenTasks)
132
+ this.svgBody.style.height = Math.max(flattenTasks.length * 30, 2000) + 'px'
121
133
  }
122
134
 
123
135
  if (changedProperties.has('locale')) {
@@ -143,16 +155,7 @@ export class OxGanttChart extends LitElement {
143
155
 
144
156
  render() {
145
157
  return html`
146
- <div
147
- chart-container
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)}
152
- @touchstart=${(e: TouchEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
153
- @touchmove=${(e: TouchEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
154
- @touchend=${(e: TouchEvent) => this.ganttChartController.handleTouchEndForBody(e)}
155
- >
158
+ <div chart-container @wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}>
156
159
  <svg
157
160
  header
158
161
  @date-range-selected=${(e: CustomEvent) => this.handleDateRangeSelected(e)}
@@ -279,88 +282,44 @@ export class OxGanttChart extends LitElement {
279
282
  initHeaderGestureHandlers() {
280
283
  const header = d3.select(this.svgHeader)
281
284
 
282
- let isDragging = false
283
285
  let startX: number | null = null
284
286
 
285
- const handleTouchStart = (event: TouchEvent) => {
287
+ const handlePointerDown = (event: PointerEvent) => {
286
288
  event.preventDefault()
287
289
  event.stopPropagation()
288
290
 
289
- isDragging = true
290
- const touch = event.touches[0]
291
291
  const containerOffsetX = header.node()!.getBoundingClientRect().left
292
- startX = touch.clientX - containerOffsetX
293
- }
294
-
295
- const handleTouchMove = (event: TouchEvent) => {
296
- event.preventDefault()
297
- event.stopPropagation()
292
+ startX = event.clientX - containerOffsetX
298
293
 
299
- if (isDragging && startX !== null) {
300
- const touch = event.touches[0]
301
- const containerOffsetX = header.node()!.getBoundingClientRect().left
302
- const currentX = touch.clientX - containerOffsetX
303
- this.highlightSelection(startX, currentX)
304
- }
294
+ window.addEventListener('pointermove', handlePointerMove)
295
+ window.addEventListener('pointerup', handlePointerUp)
305
296
  }
306
297
 
307
- const handleTouchEnd = (event: TouchEvent) => {
298
+ const handlePointerMove = (event: PointerEvent) => {
308
299
  event.preventDefault()
309
300
  event.stopPropagation()
310
301
 
311
- if (!isDragging || startX === null) {
312
- const touch = event.changedTouches[0]
313
- const containerOffsetX = header.node()!.getBoundingClientRect().left
314
- const clickX = touch.clientX - containerOffsetX
315
- this.finalizeSelection(clickX, clickX)
316
- return
317
- }
318
-
319
- const touch = event.changedTouches[0]
320
302
  const containerOffsetX = header.node()!.getBoundingClientRect().left
321
- const endX = touch.clientX - containerOffsetX
322
- this.finalizeSelection(startX, endX)
323
-
324
- isDragging = false
325
- startX = null
326
- }
303
+ const currentX = event.clientX - containerOffsetX
327
304
 
328
- const handleMouseDown = (event: MouseEvent) => {
329
- isDragging = true
330
- const containerOffsetX = header.node()!.getBoundingClientRect().left
331
- startX = event.clientX - containerOffsetX
305
+ this.highlightSelection(startX!, currentX)
332
306
  }
333
307
 
334
- const handleMouseMove = (event: MouseEvent) => {
335
- if (!isDragging || startX === null) return
336
-
337
- const containerOffsetX = header.node()!.getBoundingClientRect().left
338
- const currentX = event.clientX - containerOffsetX
339
- this.highlightSelection(startX, currentX)
340
- }
308
+ const handlePointerUp = (event: PointerEvent) => {
309
+ event.preventDefault()
310
+ event.stopPropagation()
341
311
 
342
- const handleMouseUp = (event: MouseEvent) => {
343
- if (!isDragging || startX === null) {
344
- const containerOffsetX = header.node()!.getBoundingClientRect().left
345
- const clickX = event.clientX - containerOffsetX
346
- this.finalizeSelection(clickX, clickX)
347
- return
348
- }
312
+ window.removeEventListener('pointermove', handlePointerMove)
313
+ window.removeEventListener('pointerup', handlePointerUp)
349
314
 
350
315
  const containerOffsetX = header.node()!.getBoundingClientRect().left
351
316
  const endX = event.clientX - containerOffsetX
352
- this.finalizeSelection(startX, endX)
317
+ this.finalizeSelection(startX!, endX)
353
318
 
354
- isDragging = false
355
319
  startX = null
356
320
  }
357
321
 
358
- header.on('mousedown', handleMouseDown)
359
- header.on('mousemove', handleMouseMove)
360
- header.on('mouseup', handleMouseUp)
361
- header.on('touchstart', handleTouchStart)
362
- header.on('touchmove', handleTouchMove)
363
- header.on('touchend', handleTouchEnd)
322
+ header.on('pointerdown', handlePointerDown)
364
323
  }
365
324
 
366
325
  highlightSelection(startX: number, endX: number) {
@@ -95,7 +95,7 @@ const Template: Story<ArgTypes> = ({
95
95
 
96
96
  ox-gantt {
97
97
  width: 95%;
98
- height: 650px;
98
+ height: 800px;
99
99
  box-sizing: border-box;
100
100
 
101
101
  background-color: var(--md-sys-color-primary-container);