@operato/gantt 2.0.0-beta.5 → 7.0.0-rc.10
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/CHANGELOG.md +156 -0
- package/dist/src/gantt-chart-controller.d.ts +32 -0
- package/dist/src/gantt-chart-controller.js +395 -0
- package/dist/src/gantt-chart-controller.js.map +1 -0
- package/dist/src/locales.d.ts +52 -0
- package/dist/src/locales.js +79 -0
- package/dist/src/locales.js.map +1 -0
- package/dist/src/ox-gantt-chart.d.ts +11 -20
- package/dist/src/ox-gantt-chart.js +141 -435
- package/dist/src/ox-gantt-chart.js.map +1 -1
- package/dist/src/ox-gantt-resize-splitter.d.ts +2 -3
- package/dist/src/ox-gantt-resize-splitter.js +15 -22
- package/dist/src/ox-gantt-resize-splitter.js.map +1 -1
- package/dist/src/ox-gantt.d.ts +17 -3
- package/dist/src/ox-gantt.js +169 -29
- package/dist/src/ox-gantt.js.map +1 -1
- package/dist/src/ox-task-tooltip.d.ts +12 -0
- package/dist/src/ox-task-tooltip.js +132 -0
- package/dist/src/ox-task-tooltip.js.map +1 -0
- package/dist/src/types.d.ts +11 -4
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils.d.ts +3 -3
- package/dist/src/utils.js.map +1 -1
- package/dist/stories/ox-gantt.stories.d.ts +19 -2
- package/dist/stories/ox-gantt.stories.js +104 -90
- package/dist/stories/ox-gantt.stories.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -4
- package/src/gantt-chart-controller.ts +473 -0
- package/src/locales.ts +78 -0
- package/src/ox-gantt-chart.ts +151 -484
- package/src/ox-gantt-resize-splitter.ts +18 -25
- package/src/ox-gantt.ts +174 -31
- package/src/ox-task-tooltip.ts +120 -0
- package/src/types.ts +13 -5
- package/src/utils.ts +4 -4
- package/stories/ox-gantt.stories.ts +118 -91
- package/themes/grist-theme.css +1 -3
- package/translations/en.json +11 -1
- package/translations/ja.json +11 -1
- package/translations/ko.json +11 -1
- package/translations/ms.json +11 -1
- package/translations/zh.json +11 -1
- package/dist/src/locale.d.ts +0 -1
- package/dist/src/locale.js +0 -49
- package/dist/src/locale.js.map +0 -1
- package/src/locale.ts +0 -51
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { LitElement, css, html } from 'lit'
|
|
2
|
-
|
|
3
2
|
import { customElement } from 'lit/decorators.js'
|
|
4
3
|
|
|
5
4
|
@customElement('ox-gantt-resize-splitter')
|
|
@@ -39,20 +38,7 @@ export class OxGanttResizeSplitter extends LitElement {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
render() {
|
|
42
|
-
return html
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// TODO onDrag 이벤트가 계속 발생하므로 처리하는 성능 저하됨. 그래서 throttling 하도록 함
|
|
46
|
-
_throttled(delay: number, fn: (...args: any) => void) {
|
|
47
|
-
let lastCall = 0
|
|
48
|
-
return function (...args: any) {
|
|
49
|
-
const now = new Date().getTime()
|
|
50
|
-
if (now - lastCall < delay) {
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
lastCall = now
|
|
54
|
-
return fn(...args)
|
|
55
|
-
}
|
|
41
|
+
return html`<div @mousedown=${(e: MouseEvent) => this.onMouseDown(e)}></div>`
|
|
56
42
|
}
|
|
57
43
|
|
|
58
44
|
private onMouseDown(e: MouseEvent) {
|
|
@@ -68,20 +54,33 @@ export class OxGanttResizeSplitter extends LitElement {
|
|
|
68
54
|
})
|
|
69
55
|
)
|
|
70
56
|
|
|
57
|
+
this.parentElement!.addEventListener('mousemove', this.onMouseMoveHandler)
|
|
58
|
+
this.parentElement!.addEventListener('mouseup', this.onMouseUpHandler)
|
|
71
59
|
window.addEventListener('mousemove', this.onMouseMoveHandler)
|
|
72
60
|
window.addEventListener('mouseup', this.onMouseUpHandler)
|
|
73
61
|
|
|
74
62
|
e.stopPropagation()
|
|
75
63
|
}
|
|
76
64
|
|
|
77
|
-
onMouseMoveHandler =
|
|
78
|
-
onMouseUpHandler = this.onMouseUp
|
|
65
|
+
private onMouseMoveHandler = (e: MouseEvent) => this.onMouseMove(e)
|
|
66
|
+
private onMouseUpHandler = (e: MouseEvent) => this.onMouseUp(e)
|
|
79
67
|
|
|
80
68
|
private onMouseMove(e: MouseEvent) {
|
|
81
69
|
if (!this.dragstart) {
|
|
82
70
|
return
|
|
83
71
|
}
|
|
84
72
|
|
|
73
|
+
const parentRect = this.parentElement!.getBoundingClientRect()
|
|
74
|
+
if (
|
|
75
|
+
e.clientX < parentRect.left ||
|
|
76
|
+
e.clientX > parentRect.right ||
|
|
77
|
+
e.clientY < parentRect.top ||
|
|
78
|
+
e.clientY > parentRect.bottom
|
|
79
|
+
) {
|
|
80
|
+
this.onMouseUp(e)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
85
84
|
e.preventDefault()
|
|
86
85
|
|
|
87
86
|
this.dispatchEvent(
|
|
@@ -94,9 +93,6 @@ export class OxGanttResizeSplitter extends LitElement {
|
|
|
94
93
|
}
|
|
95
94
|
})
|
|
96
95
|
)
|
|
97
|
-
requestAnimationFrame(() => {
|
|
98
|
-
dispatchEvent(new Event('resize'))
|
|
99
|
-
})
|
|
100
96
|
|
|
101
97
|
if (this.hasAttribute('vertical')) {
|
|
102
98
|
document.body.style.cursor = 'col-resize'
|
|
@@ -110,6 +106,8 @@ export class OxGanttResizeSplitter extends LitElement {
|
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
private onMouseUp(e: MouseEvent) {
|
|
109
|
+
this.parentElement!.removeEventListener('mousemove', this.onMouseMoveHandler)
|
|
110
|
+
this.parentElement!.removeEventListener('mouseup', this.onMouseUpHandler)
|
|
113
111
|
window.removeEventListener('mousemove', this.onMouseMoveHandler)
|
|
114
112
|
window.removeEventListener('mouseup', this.onMouseUpHandler)
|
|
115
113
|
|
|
@@ -124,11 +122,6 @@ export class OxGanttResizeSplitter extends LitElement {
|
|
|
124
122
|
})
|
|
125
123
|
)
|
|
126
124
|
|
|
127
|
-
requestAnimationFrame(() => {
|
|
128
|
-
dispatchEvent(new Event('resize'))
|
|
129
|
-
delete this.dragstart
|
|
130
|
-
})
|
|
131
|
-
|
|
132
125
|
document.body.style.cursor = ''
|
|
133
126
|
document.body.style.userSelect = ''
|
|
134
127
|
|
package/src/ox-gantt.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { LitElement, html, css, PropertyValues, PropertyValueMap } from 'lit'
|
|
2
|
-
import
|
|
2
|
+
import i18next from 'i18next'
|
|
3
|
+
import { customElement, property, query, state } from 'lit/decorators.js'
|
|
3
4
|
import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
|
|
4
5
|
import { ScrollbarStyles } from '@operato/styles'
|
|
5
6
|
|
|
6
7
|
import './ox-gantt-chart.js'
|
|
7
8
|
import './ox-gantt-resize-splitter.js'
|
|
8
9
|
import { OxGanttChart } from './ox-gantt-chart.js'
|
|
9
|
-
import { Task } from './types.js'
|
|
10
|
+
import { ColumnConfig, Task, TimeScale } from './types.js'
|
|
10
11
|
|
|
11
12
|
@customElement('ox-gantt')
|
|
12
13
|
export class OxGantt extends LitElement {
|
|
@@ -18,6 +19,7 @@ export class OxGantt extends LitElement {
|
|
|
18
19
|
display: block;
|
|
19
20
|
font-family: Arial, sans-serif;
|
|
20
21
|
text-wrap: nowrap;
|
|
22
|
+
position: relative;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
.container {
|
|
@@ -44,6 +46,7 @@ export class OxGantt extends LitElement {
|
|
|
44
46
|
|
|
45
47
|
ox-gantt-resize-splitter {
|
|
46
48
|
width: 4px;
|
|
49
|
+
cursor: ew-resize;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
table {
|
|
@@ -59,6 +62,9 @@ export class OxGantt extends LitElement {
|
|
|
59
62
|
top: -1px;
|
|
60
63
|
z-index: 1;
|
|
61
64
|
background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
|
|
65
|
+
border-color: var(--ox-gantt-header-border-color, var(--md-sys-color-on-secondary-container));
|
|
66
|
+
color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
|
|
67
|
+
text-transform: capitalize;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
th,
|
|
@@ -77,6 +83,10 @@ export class OxGantt extends LitElement {
|
|
|
77
83
|
height: 30px;
|
|
78
84
|
box-sizing: border-box;
|
|
79
85
|
}
|
|
86
|
+
|
|
87
|
+
tr.selected {
|
|
88
|
+
background-color: var(--ox-gantt-selected-background-color, var(--md-sys-color-surface-variant));
|
|
89
|
+
}
|
|
80
90
|
`
|
|
81
91
|
]
|
|
82
92
|
|
|
@@ -85,6 +95,51 @@ export class OxGantt extends LitElement {
|
|
|
85
95
|
fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0] /* beginning of this year */
|
|
86
96
|
@property({ type: String, attribute: 'to-date' })
|
|
87
97
|
toDate: string = new Date(new Date().getFullYear(), 11, 31).toISOString().split('T')[0] /* end of this year */
|
|
98
|
+
@property({ type: Boolean, attribute: 'extend-grid-lines' }) extendGridLines: boolean = false
|
|
99
|
+
@property({ type: String, attribute: 'time-scale' }) timeScale: TimeScale = 'week-day'
|
|
100
|
+
@property({ type: Array }) columnConfig: ColumnConfig[] = [
|
|
101
|
+
{ name: 'id', label: i18next.t('label.gantt-task-id') || 'ID', visible: true, width: 50, order: 1 },
|
|
102
|
+
{
|
|
103
|
+
name: 'section',
|
|
104
|
+
label: i18next.t('label.gantt-task-section') || 'section',
|
|
105
|
+
visible: true,
|
|
106
|
+
width: 100,
|
|
107
|
+
order: 2
|
|
108
|
+
},
|
|
109
|
+
{ name: 'title', label: i18next.t('label.gantt-task-title') || 'title', visible: true, width: 150, order: 3 },
|
|
110
|
+
{ name: 'type', label: i18next.t('label.gantt-task-type') || 'type', visible: true, width: 100, order: 4 },
|
|
111
|
+
{
|
|
112
|
+
name: 'startDate',
|
|
113
|
+
label: i18next.t('label.gantt-task-start-date') || 'start date',
|
|
114
|
+
visible: true,
|
|
115
|
+
width: 100,
|
|
116
|
+
order: 5
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'endDate',
|
|
120
|
+
label: i18next.t('label.gantt-task-end-date') || 'end date',
|
|
121
|
+
visible: true,
|
|
122
|
+
width: 100,
|
|
123
|
+
order: 6
|
|
124
|
+
},
|
|
125
|
+
{ name: 'tags', label: i18next.t('label.gantt-task-tags') || 'tags', visible: true, width: 100, order: 7 },
|
|
126
|
+
{
|
|
127
|
+
name: 'progress',
|
|
128
|
+
label: i18next.t('label.gantt-task-progress') || 'progress',
|
|
129
|
+
visible: true,
|
|
130
|
+
width: 100,
|
|
131
|
+
order: 8
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'dependsOn',
|
|
135
|
+
label: i18next.t('label.gantt-task-previous') || 'previous',
|
|
136
|
+
visible: true,
|
|
137
|
+
width: 100,
|
|
138
|
+
order: 9
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
@state() selectedTaskId?: string
|
|
88
143
|
|
|
89
144
|
@query('.container') container!: HTMLDivElement
|
|
90
145
|
@query('.task-container') taskContainer!: HTMLDivElement
|
|
@@ -93,6 +148,7 @@ export class OxGantt extends LitElement {
|
|
|
93
148
|
@query('ox-gantt-chart') ganttChart!: OxGanttChart
|
|
94
149
|
|
|
95
150
|
private _startWidth: number = 0
|
|
151
|
+
private _isDragging: boolean = false
|
|
96
152
|
|
|
97
153
|
render() {
|
|
98
154
|
return html`
|
|
@@ -102,6 +158,7 @@ export class OxGantt extends LitElement {
|
|
|
102
158
|
vertical
|
|
103
159
|
@splitter-dragstart=${(e: CustomEvent) => this.resizeStart(e)}
|
|
104
160
|
@splitter-drag=${(e: CustomEvent) => this.resizeDrag(e)}
|
|
161
|
+
@splitter-dragend=${(e: CustomEvent) => this.resizeEnd(e)}
|
|
105
162
|
></ox-gantt-resize-splitter>
|
|
106
163
|
<ox-gantt-chart
|
|
107
164
|
class="chart-container"
|
|
@@ -109,42 +166,48 @@ export class OxGantt extends LitElement {
|
|
|
109
166
|
.fromDate=${this.fromDate}
|
|
110
167
|
.toDate=${this.toDate}
|
|
111
168
|
.tasks=${this.tasks}
|
|
112
|
-
|
|
113
|
-
|
|
169
|
+
.selectedTaskId=${this.selectedTaskId}
|
|
170
|
+
.axisScale=${this.timeScale}
|
|
171
|
+
?extend-grid-lines=${this.extendGridLines}
|
|
172
|
+
@task-clicked=${this.handleTaskClicked}
|
|
114
173
|
></ox-gantt-chart>
|
|
115
174
|
</div>
|
|
116
175
|
`
|
|
117
176
|
}
|
|
118
177
|
|
|
119
178
|
renderTaskTable() {
|
|
179
|
+
if (!this.columnConfig) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const mergedTasks = this.mergeTasksBySection(this.tasks)
|
|
184
|
+
|
|
185
|
+
// Sort columns by the configured order
|
|
186
|
+
const sortedColumns = [...this.columnConfig].sort((a, b) => a.order - b.order)
|
|
187
|
+
|
|
120
188
|
return html`
|
|
121
189
|
<table>
|
|
122
190
|
<thead>
|
|
123
191
|
<tr>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<th>Type</th>
|
|
128
|
-
<th>Start Time</th>
|
|
129
|
-
<th>End Time</th>
|
|
130
|
-
<th>Tags</th>
|
|
131
|
-
<th>Progress</th>
|
|
132
|
-
<th>Prev. Task</th>
|
|
192
|
+
${sortedColumns.map(col =>
|
|
193
|
+
col.visible ? html`<th style="width: ${col.width}px">${col.label}</th>` : null
|
|
194
|
+
)}
|
|
133
195
|
</tr>
|
|
134
196
|
</thead>
|
|
135
197
|
<tbody>
|
|
136
|
-
${
|
|
137
|
-
task => html`
|
|
138
|
-
<tr>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
198
|
+
${mergedTasks.map(
|
|
199
|
+
({ task, showSection, rowSpan }) => html`
|
|
200
|
+
<tr class="${this.selectedTaskId === task.id ? 'selected' : ''}" @click=${() => this.selectTask(task.id)}>
|
|
201
|
+
${sortedColumns.map(col => {
|
|
202
|
+
if (col.name === 'section') {
|
|
203
|
+
return col.visible && showSection
|
|
204
|
+
? html`<td rowspan=${rowSpan} style="width: ${col.width}px">${task.section?.name || ''}</td>`
|
|
205
|
+
: null
|
|
206
|
+
}
|
|
207
|
+
return col.visible
|
|
208
|
+
? html`<td style="width: ${col.width}px">${this.getTaskFieldValue(task, col.name)}</td>`
|
|
209
|
+
: null
|
|
210
|
+
})}
|
|
148
211
|
</tr>
|
|
149
212
|
`
|
|
150
213
|
)}
|
|
@@ -153,25 +216,105 @@ export class OxGantt extends LitElement {
|
|
|
153
216
|
`
|
|
154
217
|
}
|
|
155
218
|
|
|
219
|
+
getTaskFieldValue(task: Task, field: string) {
|
|
220
|
+
switch (field) {
|
|
221
|
+
case 'id':
|
|
222
|
+
return task.id
|
|
223
|
+
case 'section':
|
|
224
|
+
return task.section?.name || ''
|
|
225
|
+
case 'title':
|
|
226
|
+
return task.title
|
|
227
|
+
case 'type':
|
|
228
|
+
return task.type
|
|
229
|
+
case 'startDate':
|
|
230
|
+
return new Date(task.startDate).toISOString().split('T')[0]
|
|
231
|
+
case 'endDate':
|
|
232
|
+
return new Date(task.endDate).toISOString().split('T')[0]
|
|
233
|
+
case 'tags':
|
|
234
|
+
return task.tags?.join(', ') || ''
|
|
235
|
+
case 'progress':
|
|
236
|
+
return task.progress || ''
|
|
237
|
+
case 'dependsOn':
|
|
238
|
+
return task.dependsOn || ''
|
|
239
|
+
default:
|
|
240
|
+
return ''
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
mergeTasksBySection(tasks: Task[]): Array<{ task: Task; showSection: boolean; rowSpan: number }> {
|
|
245
|
+
const mergedTasks = [] as Array<{ task: Task; showSection: boolean; rowSpan: number }>
|
|
246
|
+
let currentSection = ''
|
|
247
|
+
let rowSpan = 0
|
|
248
|
+
|
|
249
|
+
;(tasks || []).forEach((task, index) => {
|
|
250
|
+
if (task.section?.name !== currentSection) {
|
|
251
|
+
currentSection = task.section?.name || ''
|
|
252
|
+
rowSpan = 1
|
|
253
|
+
mergedTasks.push({ task, showSection: true, rowSpan: 1 })
|
|
254
|
+
} else {
|
|
255
|
+
mergedTasks[mergedTasks.length - rowSpan].rowSpan = ++rowSpan
|
|
256
|
+
mergedTasks.push({ task, showSection: false, rowSpan: 0 })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (index === tasks.length - 1 && rowSpan > 0) {
|
|
260
|
+
mergedTasks[mergedTasks.length - rowSpan].rowSpan = rowSpan
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return mergedTasks
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
selectTask(taskId: string) {
|
|
268
|
+
this.selectedTaskId = taskId
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
handleTaskClicked(event: CustomEvent) {
|
|
272
|
+
this.selectedTaskId = event.detail.taskId
|
|
273
|
+
}
|
|
274
|
+
|
|
156
275
|
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
|
157
276
|
this.taskContainer.style.width = '254px'
|
|
158
277
|
}
|
|
159
278
|
|
|
160
279
|
updated(changes: PropertyValues<this>) {}
|
|
161
280
|
|
|
162
|
-
syncScroll() {
|
|
163
|
-
|
|
281
|
+
syncScroll(e: Event) {
|
|
282
|
+
const target = e.target as HTMLElement
|
|
283
|
+
if (target === this.taskContainer) {
|
|
284
|
+
// Sync chart scroll with table scroll
|
|
285
|
+
const newScrollTop = this.taskContainer.scrollTop
|
|
286
|
+
this.ganttChart.setVerticalOffset(newScrollTop)
|
|
287
|
+
|
|
288
|
+
// Ensure chart scroll matches table scroll
|
|
289
|
+
if (this.ganttChart.getVerticalOffset() !== newScrollTop) {
|
|
290
|
+
this.ganttChart.setVerticalOffset(newScrollTop)
|
|
291
|
+
}
|
|
292
|
+
} else if (target === this.ganttChart) {
|
|
293
|
+
// Sync table scroll with chart scroll
|
|
294
|
+
const newScrollTop = this.ganttChart.getVerticalOffset()
|
|
295
|
+
this.taskContainer.scrollTop = newScrollTop
|
|
296
|
+
|
|
297
|
+
// Ensure table scroll matches chart scroll
|
|
298
|
+
this.ganttChart.setVerticalOffset(this.taskContainer.scrollTop)
|
|
299
|
+
}
|
|
164
300
|
}
|
|
165
301
|
|
|
166
302
|
resizeStart(e: CustomEvent) {
|
|
167
303
|
this._startWidth = ((e.target as HTMLElement)?.previousElementSibling as HTMLElement)?.offsetWidth
|
|
304
|
+
this._isDragging = true
|
|
168
305
|
}
|
|
169
306
|
|
|
170
307
|
resizeDrag(e: CustomEvent) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
308
|
+
if (!this._isDragging) return
|
|
309
|
+
const delta = e.detail
|
|
310
|
+
const taskContainer = this.taskContainer
|
|
311
|
+
|
|
312
|
+
if (taskContainer) {
|
|
313
|
+
taskContainer.style.width = `${this._startWidth + delta.x}px`
|
|
175
314
|
}
|
|
176
315
|
}
|
|
316
|
+
|
|
317
|
+
resizeEnd(e: CustomEvent) {
|
|
318
|
+
this._isDragging = false
|
|
319
|
+
}
|
|
177
320
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
|
|
4
|
+
@customElement('ox-task-tooltip')
|
|
5
|
+
export class OxTaskTooltip extends LitElement {
|
|
6
|
+
static styles = css`
|
|
7
|
+
:host {
|
|
8
|
+
position: fixed; /* position: absolute로 변경 */
|
|
9
|
+
background-color: var(--ox-gantt-tooltip-background-color, var(--md-sys-color-surface, #f9f9f9));
|
|
10
|
+
border: 1px solid var(--ox-gantt-tooltip-border-color, var(--md-sys-color-outline, #ccc));
|
|
11
|
+
padding: var(--spacing-medium, 10px);
|
|
12
|
+
border-radius: var(--spacing-medium, 8px);
|
|
13
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
14
|
+
visibility: hidden;
|
|
15
|
+
z-index: 10;
|
|
16
|
+
max-width: 300px;
|
|
17
|
+
font-family: Roboto;
|
|
18
|
+
font-size: 14px;
|
|
19
|
+
line-height: 1.6;
|
|
20
|
+
color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
|
|
21
|
+
transition:
|
|
22
|
+
visibility 0.1s,
|
|
23
|
+
opacity 0.1s;
|
|
24
|
+
opacity: 0; /* 초기에는 보이지 않음 */
|
|
25
|
+
}
|
|
26
|
+
:host([visible]) {
|
|
27
|
+
visibility: visible;
|
|
28
|
+
opacity: 1; /* 보일 때는 불투명 */
|
|
29
|
+
}
|
|
30
|
+
.title {
|
|
31
|
+
font-weight: bold;
|
|
32
|
+
font-size: 16px;
|
|
33
|
+
margin-bottom: 8px;
|
|
34
|
+
color: var(--ox-gantt-tooltip-title-color, var(--md-sys-color-primary, #0078d4)); /* 파란색 텍스트 */
|
|
35
|
+
}
|
|
36
|
+
.detail {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: auto auto 1fr;
|
|
39
|
+
gap: 4px;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
.label {
|
|
43
|
+
font-weight: bold;
|
|
44
|
+
color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface-variant, #333));
|
|
45
|
+
text-align: right; /* 라벨을 우측 정렬 */
|
|
46
|
+
}
|
|
47
|
+
.colon {
|
|
48
|
+
text-align: center;
|
|
49
|
+
color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
|
|
50
|
+
}
|
|
51
|
+
.value {
|
|
52
|
+
text-align: left; /* 값을 좌측 정렬 */
|
|
53
|
+
color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
@property({ type: String }) title = ''
|
|
58
|
+
@property({ type: String }) startDate = ''
|
|
59
|
+
@property({ type: String }) endDate = ''
|
|
60
|
+
@property({ type: Number }) progress = 0
|
|
61
|
+
@property({ type: String }) type = ''
|
|
62
|
+
|
|
63
|
+
show(x: number, y: number) {
|
|
64
|
+
const tooltipWidth = this.offsetWidth
|
|
65
|
+
const tooltipHeight = this.offsetHeight
|
|
66
|
+
const offsetX = 10
|
|
67
|
+
const offsetY = 10
|
|
68
|
+
|
|
69
|
+
// 기본 위치 계산
|
|
70
|
+
let left = x + offsetX
|
|
71
|
+
let top = y + offsetY
|
|
72
|
+
|
|
73
|
+
// 화면 경계 확인 및 조정
|
|
74
|
+
const screenWidth = window.innerWidth
|
|
75
|
+
const screenHeight = window.innerHeight
|
|
76
|
+
|
|
77
|
+
if (left + tooltipWidth > screenWidth) {
|
|
78
|
+
left = x - tooltipWidth - offsetX
|
|
79
|
+
}
|
|
80
|
+
if (top + tooltipHeight > screenHeight) {
|
|
81
|
+
top = y - tooltipHeight - offsetY
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// offsetParent 요소를 기준으로 위치 설정
|
|
85
|
+
if (this.offsetParent) {
|
|
86
|
+
const parentRect = this.offsetParent!.getBoundingClientRect()
|
|
87
|
+
this.style.left = `${left - parentRect.left}px`
|
|
88
|
+
this.style.top = `${top - parentRect.top}px`
|
|
89
|
+
} else {
|
|
90
|
+
this.style.left = `${left}px`
|
|
91
|
+
this.style.top = `${top}px`
|
|
92
|
+
|
|
93
|
+
const myRect = this.getBoundingClientRect()
|
|
94
|
+
|
|
95
|
+
/* 실제 바운드와 설정한 left/top 이 일치하지 않는다면, 부모 어딘가에서 transform 되었다고 판단하고, 보정해줌 */
|
|
96
|
+
if (myRect.left !== left || myRect.top !== top) {
|
|
97
|
+
this.style.left = `${left + (left - myRect.left)}px`
|
|
98
|
+
this.style.top = `${top + (top - myRect.top)}px`
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.setAttribute('visible', '')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
hide() {
|
|
106
|
+
this.removeAttribute('visible')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
render() {
|
|
110
|
+
return html`
|
|
111
|
+
<div class="title">${this.title}</div>
|
|
112
|
+
<div class="detail">
|
|
113
|
+
<span class="label">Type</span><span class="colon">:</span><span class="value">${this.type}</span>
|
|
114
|
+
<span class="label">Start Date</span><span class="colon">:</span><span class="value">${this.startDate}</span>
|
|
115
|
+
<span class="label">End Date</span><span class="colon">:</span><span class="value">${this.endDate}</span>
|
|
116
|
+
<span class="label">Progress</span><span class="colon">:</span><span class="value">${this.progress}%</span>
|
|
117
|
+
</div>
|
|
118
|
+
`
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -6,11 +6,11 @@ export interface Task {
|
|
|
6
6
|
title: string
|
|
7
7
|
type: TaskType
|
|
8
8
|
section?: Section
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
startDate: Date | string
|
|
10
|
+
endDate: Date | string
|
|
11
11
|
progress?: number
|
|
12
|
-
|
|
13
|
-
length: string
|
|
12
|
+
dependsOn?: string
|
|
13
|
+
length: string
|
|
14
14
|
tags?: TaskTag[]
|
|
15
15
|
children?: Task[]
|
|
16
16
|
}
|
|
@@ -19,4 +19,12 @@ export interface Section {
|
|
|
19
19
|
name: string
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export
|
|
22
|
+
export interface ColumnConfig {
|
|
23
|
+
name: string
|
|
24
|
+
label: string
|
|
25
|
+
visible: boolean
|
|
26
|
+
width: number
|
|
27
|
+
order: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type TimeScale = 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
|
package/src/utils.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TimeScale } from './types'
|
|
2
2
|
|
|
3
|
-
const levels:
|
|
3
|
+
const levels: TimeScale[] = ['week-day', 'quarter-week', 'year-month', 'year-quarter']
|
|
4
4
|
|
|
5
|
-
export function levelUp(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'):
|
|
5
|
+
export function levelUp(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'): TimeScale {
|
|
6
6
|
const current = Math.min(levels.length, levels.indexOf(level) + 1)
|
|
7
7
|
return levels[current]
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function levelDown(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'):
|
|
10
|
+
export function levelDown(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'): TimeScale {
|
|
11
11
|
const current = Math.max(0, levels.indexOf(level) - 1)
|
|
12
12
|
return levels[current]
|
|
13
13
|
}
|