@operato/gantt 8.0.0-alpha.4 → 8.0.0-alpha.43

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/src/gantt-chart-controller.d.ts +5 -6
  3. package/dist/src/gantt-chart-controller.js +92 -37
  4. package/dist/src/gantt-chart-controller.js.map +1 -1
  5. package/dist/src/ox-gantt-chart.d.ts +374 -3
  6. package/dist/src/ox-gantt-chart.js +86 -180
  7. package/dist/src/ox-gantt-chart.js.map +1 -1
  8. package/dist/src/ox-gantt-resize-splitter.d.ts +6 -2
  9. package/dist/src/ox-gantt-resize-splitter.js +70 -39
  10. package/dist/src/ox-gantt-resize-splitter.js.map +1 -1
  11. package/dist/src/ox-gantt.js +10 -1
  12. package/dist/src/ox-gantt.js.map +1 -1
  13. package/dist/src/ox-resource-chart.js +2 -0
  14. package/dist/src/ox-resource-chart.js.map +1 -1
  15. package/dist/src/ox-task-tooltip.d.ts +3 -1
  16. package/dist/src/ox-task-tooltip.js +36 -11
  17. package/dist/src/ox-task-tooltip.js.map +1 -1
  18. package/dist/stories/ox-gantt.stories.js +17 -1
  19. package/dist/stories/ox-gantt.stories.js.map +1 -1
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/package.json +13 -13
  22. package/src/gantt-chart-controller.ts +100 -34
  23. package/src/ox-gantt-chart.ts +92 -192
  24. package/src/ox-gantt-resize-splitter.ts +72 -44
  25. package/src/ox-gantt.ts +10 -1
  26. package/src/ox-resource-chart.ts +2 -0
  27. package/src/ox-task-tooltip.ts +31 -9
  28. package/stories/ox-gantt.stories.ts +17 -1
  29. package/themes/calendar-theme.css +3 -1
  30. package/assets/images/icon-editor-gradient-direction.png +0 -0
  31. package/assets/images/icon-properties-label.png +0 -0
  32. package/assets/images/icon-properties-line-type.png +0 -0
  33. package/assets/images/icon-properties-table.png +0 -0
@@ -3,19 +3,16 @@ 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'
7
+ import { GestureEventPinch, GestureEventDoubleTap, GestureEventDrag } from '@operato/utils/gesture-mixin.js'
6
8
 
7
9
  export class GanttChartController implements ReactiveController {
8
- private host: ReactiveControllerHost & LitElement
10
+ private host: (ReactiveControllerHost & LitElement) | null
9
11
  private fromTime: Date
10
12
  private toTime: Date
11
13
  private totalDays: number
12
14
  private _timeScale?: d3.ScaleTime<number, number, never>
13
15
 
14
- private touchStartX?: number
15
- private touchStartY?: number
16
- private scrollStartX?: number
17
- private scrollStartY?: number
18
-
19
16
  private colorProvider?: (task: Task) => string
20
17
 
21
18
  public baseWidth: number = 30
@@ -34,6 +31,10 @@ export class GanttChartController implements ReactiveController {
34
31
  this.host.addController(this)
35
32
  }
36
33
 
34
+ dispose() {
35
+ this.host = null
36
+ }
37
+
37
38
  hostConnected() {
38
39
  this.updateTimeScale()
39
40
  }
@@ -57,12 +58,12 @@ export class GanttChartController implements ReactiveController {
57
58
  this.toTime = new Date(toDate)
58
59
  this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
59
60
  this._timeScale = undefined
60
- this.host.requestUpdate()
61
+ this.host?.requestUpdate()
61
62
  }
62
63
 
63
64
  setTasks(tasks: Task[]) {
64
65
  this.tasks = tasks
65
- this.host.requestUpdate()
66
+ this.host?.requestUpdate()
66
67
  }
67
68
 
68
69
  updateTimeScale() {
@@ -93,7 +94,7 @@ export class GanttChartController implements ReactiveController {
93
94
  .append('marker')
94
95
  .attr('id', 'arrow')
95
96
  .attr('viewBox', '0 0 10 10')
96
- .attr('refX', 10) // Adjusted refX to move the arrow tip closer to the line's end
97
+ .attr('refX', 10)
97
98
  .attr('refY', 5)
98
99
  .attr('markerWidth', 6)
99
100
  .attr('markerHeight', 6)
@@ -150,14 +151,20 @@ export class GanttChartController implements ReactiveController {
150
151
  const taskWidth = endX - startX + this.baseWidth / this.getScale()
151
152
  const taskY = index * 30 + 7.5
152
153
 
153
- const showTooltip = (event: MouseEvent) => {
154
+ const showTooltip = (event: MouseEvent | TouchEvent) => {
155
+ tooltip.hide()
156
+
157
+ const touch = 'touches' in event
158
+ const x = touch ? event.touches[0].clientX : event.clientX
159
+ const y = touch ? event.touches[0].clientY : event.clientY
160
+
154
161
  tooltip.title = task.title
155
162
  tooltip.startDate = startDate.toISOString().split('T')[0]
156
163
  tooltip.endDate = endDate.toISOString().split('T')[0]
157
164
  tooltip.progress = task.progress ?? 0
158
165
  tooltip.type = task.type
159
166
 
160
- tooltip.show(event.clientX, event.clientY)
167
+ tooltip.show(x, y)
161
168
  }
162
169
 
163
170
  const hideTooltip = () => {
@@ -185,15 +192,18 @@ export class GanttChartController implements ReactiveController {
185
192
  .attr('stroke-width', 1)
186
193
 
187
194
  taskBar
188
- .on('click', () => {
195
+ .on('click', e => {
189
196
  this.selectedTaskId = task.id
190
197
  const customEvent = new CustomEvent('task-clicked', {
191
198
  detail: { taskId: task.id },
192
199
  bubbles: true,
193
200
  composed: true
194
201
  })
195
- this.host.dispatchEvent(customEvent)
196
- this.host.requestUpdate()
202
+
203
+ showTooltip(e)
204
+
205
+ this.host?.dispatchEvent(customEvent)
206
+ this.host?.requestUpdate()
197
207
  })
198
208
  .on('mouseover', showTooltip)
199
209
  .on('mouseout', hideTooltip)
@@ -209,15 +219,18 @@ export class GanttChartController implements ReactiveController {
209
219
  .attr('fill', 'lightgreen')
210
220
 
211
221
  progressBar
212
- .on('click', () => {
222
+ .on('click', e => {
213
223
  this.selectedTaskId = task.id
214
224
  const customEvent = new CustomEvent('task-clicked', {
215
225
  detail: { taskId: task.id },
216
226
  bubbles: true,
217
227
  composed: true
218
228
  })
219
- this.host.dispatchEvent(customEvent)
220
- this.host.requestUpdate()
229
+
230
+ showTooltip(e)
231
+
232
+ this.host?.dispatchEvent(customEvent)
233
+ this.host?.requestUpdate()
221
234
  })
222
235
  .on('mouseover', showTooltip)
223
236
  .on('mouseout', hideTooltip)
@@ -256,7 +269,7 @@ export class GanttChartController implements ReactiveController {
256
269
  .attr('stroke', 'var(--md-sys-color-on-primary-container, black)')
257
270
  .attr('fill', 'none')
258
271
  .attr('stroke-width', 1)
259
- .attr('marker-end', 'url(#arrow)') // Add arrow marker
272
+ .attr('marker-end', 'url(#arrow)')
260
273
  }
261
274
  })
262
275
  }
@@ -277,6 +290,8 @@ export class GanttChartController implements ReactiveController {
277
290
  event.preventDefault()
278
291
  event.stopPropagation()
279
292
 
293
+ const { offsetHeight } = container
294
+
280
295
  const deltaModeFactor =
281
296
  event.deltaMode === WheelEvent.DOM_DELTA_LINE
282
297
  ? 16
@@ -288,7 +303,8 @@ export class GanttChartController implements ReactiveController {
288
303
 
289
304
  // 가로 스크롤 및 세로 스크롤 모두 처리
290
305
  container.scrollLeft += deltaX
291
- container.scrollTop += deltaY
306
+ // container.scrollTop += deltaY
307
+ container.scrollTop = Math.min(this.tasks.length * 30 - (offsetHeight - 40), container.scrollTop + deltaY)
292
308
 
293
309
  const customEvent = new CustomEvent('scroll', {
294
310
  detail: {
@@ -299,28 +315,78 @@ export class GanttChartController implements ReactiveController {
299
315
  composed: true
300
316
  })
301
317
 
302
- this.host.dispatchEvent(customEvent)
318
+ this.host?.dispatchEvent(customEvent)
303
319
  }
304
320
 
305
- handleTouchStartForBody(event: TouchEvent, container: HTMLDivElement) {
306
- event.stopPropagation()
321
+ handlePinchForBody = (event: GestureEventPinch) => {
322
+ const { scale } = event.detail
323
+
324
+ this.baseWidth *= scale
325
+
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
+ }
307
338
 
308
- this.touchStartX = event.touches[0].clientX
309
- this.touchStartY = event.touches[0].clientY
339
+ if (this.baseWidth < 20) {
340
+ this.level = levelUp(this.level)
341
+ this.baseWidth = 30
342
+ }
310
343
 
311
- this.scrollStartX = container.scrollLeft
312
- this.scrollStartY = container.scrollTop
344
+ this.updateTimeScale()
345
+ this.host?.requestUpdate()
346
+
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
+ })
355
+
356
+ this.host?.dispatchEvent(customEvent)
313
357
  }
314
358
 
315
- handleTouchMoveForBody(event: TouchEvent, container: HTMLDivElement) {
316
- const touchCurrentX = event.touches[0].clientX
317
- const touchCurrentY = event.touches[0].clientY
359
+ handleDoubleTapForBody = (event: GestureEventDoubleTap) => {
360
+ const container = (this.host as any).container
361
+
362
+ container.scrollLeft = 0
363
+ container.scrollTop = 0
364
+
365
+ this.baseWidth = 30
366
+ this.level = 'week-day'
367
+
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)
381
+ }
318
382
 
319
- const deltaX = this.touchStartX! - touchCurrentX
320
- const deltaY = this.touchStartY! - touchCurrentY
383
+ handleDragForBody = (event: GestureEventDrag) => {
384
+ const { deltaX: dx, deltaY: dy } = event.detail
385
+ const container = (this.host as any).container
386
+ const { offsetHeight } = container
321
387
 
322
- container.scrollLeft = this.scrollStartX! + deltaX
323
- container.scrollTop = this.scrollStartY! + deltaY
388
+ container.scrollLeft -= dx
389
+ container.scrollTop = Math.min(this.tasks.length * 30 - (offsetHeight - 40), container.scrollTop - dy)
324
390
 
325
391
  const customEvent = new CustomEvent('scroll', {
326
392
  detail: {
@@ -331,6 +397,6 @@ export class GanttChartController implements ReactiveController {
331
397
  composed: true
332
398
  })
333
399
 
334
- this.host.dispatchEvent(customEvent)
400
+ this.host?.dispatchEvent(customEvent)
335
401
  }
336
402
  }
@@ -2,7 +2,7 @@ import './ox-task-tooltip.js'
2
2
  import './ox-resource-chart.js'
3
3
 
4
4
  import { LitElement, html, css, PropertyValues, PropertyValueMap } from 'lit'
5
- import { customElement, property, query } from 'lit/decorators.js'
5
+ import { customElement, property, query, state } from 'lit/decorators.js'
6
6
  import * as d3 from 'd3'
7
7
  import { locales } from './locales'
8
8
 
@@ -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,
@@ -23,15 +25,21 @@ export class OxGanttChart extends LitElement {
23
25
  display: flex;
24
26
  overflow: hidden;
25
27
  position: relative;
28
+ user-select: none;
26
29
  }
30
+
27
31
  div[chart-container] {
28
32
  display: block;
29
33
  overflow-x: auto;
30
34
  overflow-y: hidden;
35
+ overscroll-behavior: none;
36
+ touch-action: none;
31
37
  }
38
+
32
39
  svg {
33
40
  min-width: 100%;
34
41
  }
42
+
35
43
  svg[header] {
36
44
  display: block;
37
45
  min-width: 100%;
@@ -44,27 +52,27 @@ export class OxGanttChart extends LitElement {
44
52
  top: 0;
45
53
  z-index: 1;
46
54
  }
47
- .scroll-buttons {
48
- position: absolute;
49
- top: 44px; /* 헤더 바로 아래 위치 */
50
- right: 4px;
51
- display: flex;
52
- align-items: center;
53
- gap: 2px;
54
- }
55
+
55
56
  .scroll-button {
56
- width: 10px;
57
- height: 10px;
57
+ position: absolute;
58
+ top: 46px; /* 헤더 바로 아래 위치 */
59
+ width: 14px;
60
+ height: 14px;
58
61
  background-color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
62
+ opacity: 0.5;
59
63
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
60
64
  cursor: pointer;
61
65
  transform: rotate(90deg);
62
66
  }
67
+
63
68
  .scroll-button.left {
69
+ left: 6px;
64
70
  transform: rotate(-90deg);
65
71
  }
72
+
66
73
  .scroll-button.right {
67
74
  transform: rotate(90deg);
75
+ right: 6px;
68
76
  }
69
77
  `
70
78
  ]
@@ -81,20 +89,38 @@ export class OxGanttChart extends LitElement {
81
89
  @property({ type: String }) locale: string = 'en-US'
82
90
  @property({ type: Object }) colorProvider!: (task: Task) => string
83
91
 
92
+ @state() dateRangeSelected?: { start: Date; end: Date }
93
+
84
94
  @query('svg[header]') svgHeader!: SVGElement
85
95
  @query('svg[body]') svgBody!: SVGElement
86
96
  @query('[chart-container]') container!: HTMLDivElement
87
97
  @query('ox-task-tooltip') tooltip!: OxTaskTooltip
88
98
 
89
- private ganttChartController: GanttChartController
99
+ private ganttChartController!: GanttChartController
100
+
101
+ protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
102
+ this.initHeaderGestureHandlers()
103
+
104
+ // this.addEventListener('date-range-cleared', e => this.clearSelection())
105
+ }
90
106
 
91
- constructor() {
92
- super()
107
+ connectedCallback(): void {
108
+ super.connectedCallback()
93
109
  this.ganttChartController = new GanttChartController(this, this.fromDate, this.toDate, this.axisScale)
110
+
111
+ this.addEventListener('pinch', this.ganttChartController.handlePinchForBody as EventListener)
112
+ this.addEventListener('drag', this.ganttChartController.handleDragForBody as EventListener)
113
+ this.addEventListener('doubletap', this.ganttChartController.handleDoubleTapForBody as EventListener)
94
114
  }
95
115
 
96
- protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
97
- this.initHeaderGestureHandlers()
116
+ disconnectedCallback(): void {
117
+ super.disconnectedCallback()
118
+
119
+ this.removeEventListener('pinch', this.ganttChartController.handlePinchForBody as EventListener)
120
+ this.removeEventListener('drag', this.ganttChartController.handleDragForBody as EventListener)
121
+ this.removeEventListener('doubletap', this.ganttChartController.handleDoubleTapForBody as EventListener)
122
+
123
+ this.ganttChartController.dispose()
98
124
  }
99
125
 
100
126
  updated(changedProperties: PropertyValues<this>) {
@@ -103,8 +129,9 @@ export class OxGanttChart extends LitElement {
103
129
  }
104
130
 
105
131
  if (changedProperties.has('tasks')) {
106
- this.ganttChartController.setTasks(this.flattenTasks(this.tasks))
107
- this.svgBody.style.height = Math.max(this.tasks.length * 30, 2000) + 'px'
132
+ const flattenTasks = this.flattenTasks(this.tasks)
133
+ this.ganttChartController.setTasks(flattenTasks)
134
+ this.svgBody.style.height = Math.max(flattenTasks.length * 30, 2000) + 'px'
108
135
  }
109
136
 
110
137
  if (changedProperties.has('locale')) {
@@ -126,26 +153,16 @@ export class OxGanttChart extends LitElement {
126
153
 
127
154
  this.ganttChartController.drawAxes(this.svgHeader)
128
155
  this.ganttChartController.drawBody(this.svgBody, this.tooltip)
156
+ this.drawSelectedRange()
129
157
  }
130
158
 
131
159
  render() {
132
160
  return html`
133
- <div
134
- chart-container
135
- @wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}
136
- @touchstart=${(e: TouchEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
137
- @touchmove=${(e: TouchEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
138
- >
139
- <svg
140
- header
141
- @date-range-selected=${(e: CustomEvent) => this.handleDateRangeSelected(e)}
142
- @wheel=${this.handleWheelForHeader.bind(this)}
143
- ></svg>
161
+ <div chart-container @wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}>
162
+ <svg header @wheel=${this.handleWheelForHeader.bind(this)}></svg>
144
163
  <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>
164
+ <div class="scroll-button left" @click=${this.scrollToStart} @touchstart=${this.scrollToStart}></div>
165
+ <div class="scroll-button right" @click=${this.scrollToEnd} @touchstart=${this.scrollToEnd}></div>
149
166
  <ox-task-tooltip></ox-task-tooltip>
150
167
  </div>
151
168
  `
@@ -248,11 +265,6 @@ export class OxGanttChart extends LitElement {
248
265
  this.dispatchEvent(customEvent)
249
266
  }
250
267
 
251
- handleDateRangeSelected(event: CustomEvent) {
252
- const detail = event.detail
253
- this.dispatchEvent(new CustomEvent('date-range-selected', { detail }))
254
- }
255
-
256
268
  updateLocale(locale: string) {
257
269
  const localeDefinition = (locales as any)[locale || 'en-US']
258
270
  if (localeDefinition) {
@@ -264,141 +276,47 @@ export class OxGanttChart extends LitElement {
264
276
  initHeaderGestureHandlers() {
265
277
  const header = d3.select(this.svgHeader)
266
278
 
267
- let isDragging = false
268
279
  let startX: number | null = null
269
- let lastTouchDistance: number | null = null
270
280
 
271
- const handleTouchStart = (event: TouchEvent) => {
281
+ const handlePointerDown = (event: PointerEvent) => {
272
282
  event.preventDefault()
283
+ event.stopPropagation()
273
284
 
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
- }
282
- }
283
-
284
- const handleTouchMove = (event: TouchEvent) => {
285
- event.preventDefault()
285
+ const containerOffsetX = header.node()!.getBoundingClientRect().left
286
+ startX = event.clientX - containerOffsetX
286
287
 
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) {
323
- const touch = event.touches[0]
324
- const containerOffsetX = header.node()!.getBoundingClientRect().left
325
- const currentX = touch.clientX - containerOffsetX
326
- this.highlightSelection(startX, currentX)
327
- }
288
+ window.addEventListener('pointermove', handlePointerMove)
289
+ window.addEventListener('pointerup', handlePointerUp)
328
290
  }
329
291
 
330
- const handleTouchEnd = (event: TouchEvent) => {
292
+ const handlePointerMove = (event: PointerEvent) => {
331
293
  event.preventDefault()
294
+ event.stopPropagation()
332
295
 
333
- if (event.touches.length < 2) {
334
- lastTouchDistance = null
335
- }
336
-
337
- if (!isDragging || startX === null) {
338
- const touch = event.changedTouches[0]
339
- const containerOffsetX = header.node()!.getBoundingClientRect().left
340
- const clickX = touch.clientX - containerOffsetX
341
- this.finalizeSelection(clickX, clickX)
342
- return
343
- }
344
-
345
- const touch = event.changedTouches[0]
346
296
  const containerOffsetX = header.node()!.getBoundingClientRect().left
347
- const endX = touch.clientX - containerOffsetX
348
- this.finalizeSelection(startX, endX)
349
-
350
- isDragging = false
351
- startX = null
352
- }
297
+ const currentX = event.clientX - containerOffsetX
353
298
 
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)
299
+ this.highlightSelection(startX!, currentX)
358
300
  }
359
301
 
360
- const handleMouseDown = (event: MouseEvent) => {
361
- isDragging = true
362
- const containerOffsetX = header.node()!.getBoundingClientRect().left
363
- startX = event.clientX - containerOffsetX
364
- }
365
-
366
- const handleMouseMove = (event: MouseEvent) => {
367
- if (!isDragging || startX === null) return
368
-
369
- const containerOffsetX = header.node()!.getBoundingClientRect().left
370
- const currentX = event.clientX - containerOffsetX
371
- this.highlightSelection(startX, currentX)
372
- }
302
+ const handlePointerUp = (event: PointerEvent) => {
303
+ event.preventDefault()
304
+ event.stopPropagation()
373
305
 
374
- const handleMouseUp = (event: MouseEvent) => {
375
- if (!isDragging || startX === null) {
376
- const containerOffsetX = header.node()!.getBoundingClientRect().left
377
- const clickX = event.clientX - containerOffsetX
378
- this.finalizeSelection(clickX, clickX)
379
- return
380
- }
306
+ window.removeEventListener('pointermove', handlePointerMove)
307
+ window.removeEventListener('pointerup', handlePointerUp)
381
308
 
382
309
  const containerOffsetX = header.node()!.getBoundingClientRect().left
383
310
  const endX = event.clientX - containerOffsetX
384
- this.finalizeSelection(startX, endX)
311
+ this.finalizeSelection(startX!, endX)
385
312
 
386
- isDragging = false
387
313
  startX = null
388
314
  }
389
315
 
390
- header.on('mousedown', handleMouseDown)
391
- header.on('mousemove', handleMouseMove)
392
- header.on('mouseup', handleMouseUp)
393
- header.on('touchstart', handleTouchStart)
394
- header.on('touchmove', handleTouchMove)
395
- header.on('touchend', handleTouchEnd)
316
+ header.on('pointerdown', handlePointerDown)
396
317
  }
397
318
 
398
319
  highlightSelection(startX: number, endX: number) {
399
- const header = d3.select(this.svgHeader)
400
- const body = d3.select(this.svgBody)
401
-
402
320
  let startDate = this.ganttChartController.timeScale.invert(startX)
403
321
  let endDate = this.ganttChartController.timeScale.invert(endX)
404
322
 
@@ -418,44 +336,10 @@ export class OxGanttChart extends LitElement {
418
336
  end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
419
337
  }
420
338
 
421
- header.selectAll('.selection-rect').remove()
422
- body.selectAll('.selection-rect').remove()
423
-
424
- header
425
- .append('rect')
426
- .attr('class', 'selection-rect')
427
- .attr('x', this.ganttChartController.timeScale(start!))
428
- .attr('y', 20)
429
- .attr(
430
- 'width',
431
- this.ganttChartController.timeScale(end!) -
432
- this.ganttChartController.timeScale(start!) +
433
- this.ganttChartController.baseWidth / this.ganttChartController.getScale()
434
- )
435
- .attr('height', 20)
436
- .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-on-primary-container))')
437
- .attr('opacity', '.2')
438
-
439
- body
440
- .append('rect')
441
- .attr('class', 'selection-rect')
442
- .attr('x', this.ganttChartController.timeScale(start!))
443
- .attr('y', 0)
444
- .attr(
445
- 'width',
446
- this.ganttChartController.timeScale(end!) -
447
- this.ganttChartController.timeScale(start!) +
448
- this.ganttChartController.baseWidth / this.ganttChartController.getScale()
449
- )
450
- .attr('height', '100%')
451
- .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-on-primary-container))')
452
- .attr('opacity', '.2')
339
+ this.dateRangeSelected = { start: start!, end: end! }
453
340
  }
454
341
 
455
342
  finalizeSelection(startX: number, endX: number) {
456
- const header = d3.select(this.svgHeader)
457
- const body = d3.select(this.svgBody)
458
-
459
343
  let startDate = this.ganttChartController.timeScale.invert(startX)
460
344
  let endDate = this.ganttChartController.timeScale.invert(endX)
461
345
 
@@ -475,6 +359,29 @@ export class OxGanttChart extends LitElement {
475
359
  end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
476
360
  }
477
361
 
362
+ this.dateRangeSelected = { start: start!, end: end! }
363
+
364
+ const formatDate = d3.timeFormat('%Y-%m-%d')
365
+
366
+ this.dispatchEvent(
367
+ new CustomEvent('date-range-selected', {
368
+ detail: { start: formatDate(start!), end: formatDate(end!) },
369
+ bubbles: true,
370
+ composed: true
371
+ })
372
+ )
373
+ }
374
+
375
+ drawSelectedRange() {
376
+ if (!this.dateRangeSelected) {
377
+ return
378
+ }
379
+
380
+ let { start, end } = this.dateRangeSelected
381
+
382
+ const header = d3.select(this.svgHeader)
383
+ const body = d3.select(this.svgBody)
384
+
478
385
  header.selectAll('.selection-rect').remove()
479
386
  body.selectAll('.selection-rect').remove()
480
387
 
@@ -507,12 +414,5 @@ export class OxGanttChart extends LitElement {
507
414
  .attr('height', '100%')
508
415
  .attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-on-primary-container))')
509
416
  .attr('opacity', '.2')
510
-
511
- const formatDate = d3.timeFormat('%Y-%m-%d')
512
- const customEvent = new CustomEvent('date-range-selected', {
513
- detail: { start: formatDate(start!), end: formatDate(end!) }
514
- })
515
-
516
- header.node()?.dispatchEvent(customEvent)
517
417
  }
518
418
  }