@operato/gantt 2.0.0-beta.13 → 2.0.0-beta.15
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 +28 -0
- package/dist/src/gantt-chart-controller.d.ts +32 -0
- package/dist/src/gantt-chart-controller.js +393 -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 +10 -17
- package/dist/src/ox-gantt-chart.js +135 -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 +14 -1
- package/dist/src/ox-gantt.js +149 -28
- 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 +10 -3
- package/dist/src/types.js.map +1 -1
- package/dist/stories/ox-gantt.stories.d.ts +12 -0
- package/dist/stories/ox-gantt.stories.js +100 -88
- package/dist/stories/ox-gantt.stories.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/gantt-chart-controller.ts +470 -0
- package/src/locales.ts +78 -0
- package/src/ox-gantt-chart.ts +143 -481
- package/src/ox-gantt-resize-splitter.ts +18 -25
- package/src/ox-gantt.ts +153 -30
- package/src/ox-task-tooltip.ts +120 -0
- package/src/types.ts +11 -3
- package/stories/ox-gantt.stories.ts +105 -88
- 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
package/src/ox-gantt-chart.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import './ox-task-tooltip'
|
|
2
|
+
|
|
1
3
|
import { LitElement, html, css, PropertyValues, PropertyValueMap } from 'lit'
|
|
2
|
-
import { customElement, property, query
|
|
3
|
-
import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
|
|
4
|
+
import { customElement, property, query } from 'lit/decorators.js'
|
|
4
5
|
import * as d3 from 'd3'
|
|
6
|
+
import { locales } from './locales'
|
|
5
7
|
|
|
6
|
-
import {
|
|
7
|
-
import { levelDown, levelUp } from './utils'
|
|
8
|
-
|
|
8
|
+
import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
|
|
9
9
|
import { ScrollbarStyles } from '@operato/styles'
|
|
10
|
-
|
|
10
|
+
import { GanttChartController } from './gantt-chart-controller'
|
|
11
11
|
import { Task } from './types.js'
|
|
12
|
+
import { levelDown, levelUp } from './utils'
|
|
13
|
+
import { OxTaskTooltip } from './ox-task-tooltip.js'
|
|
12
14
|
|
|
13
15
|
type TimeScale = 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
|
|
14
16
|
|
|
@@ -20,23 +22,17 @@ export class OxGanttChart extends LitElement {
|
|
|
20
22
|
css`
|
|
21
23
|
:host {
|
|
22
24
|
display: flex;
|
|
23
|
-
|
|
24
25
|
overflow: hidden;
|
|
26
|
+
position: relative;
|
|
25
27
|
}
|
|
26
|
-
|
|
27
28
|
div[chart-container] {
|
|
28
29
|
display: block;
|
|
29
|
-
|
|
30
30
|
overflow-x: auto;
|
|
31
|
-
overflow-y:
|
|
32
|
-
|
|
33
|
-
position: relative;
|
|
31
|
+
overflow-y: hidden;
|
|
34
32
|
}
|
|
35
|
-
|
|
36
33
|
svg {
|
|
37
34
|
min-width: 100%;
|
|
38
35
|
}
|
|
39
|
-
|
|
40
36
|
svg[header] {
|
|
41
37
|
display: block;
|
|
42
38
|
min-width: 100%;
|
|
@@ -44,467 +40,152 @@ export class OxGanttChart extends LitElement {
|
|
|
44
40
|
border-bottom: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
|
|
45
41
|
height: 40px;
|
|
46
42
|
background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
|
|
47
|
-
|
|
48
43
|
position: sticky;
|
|
49
44
|
top: 0;
|
|
50
45
|
z-index: 1;
|
|
51
46
|
}
|
|
52
|
-
|
|
53
47
|
.top-axis text,
|
|
54
48
|
.bottom-axis text {
|
|
55
49
|
font-size: 12px;
|
|
56
50
|
}
|
|
57
|
-
|
|
58
51
|
.grid line {
|
|
59
52
|
stroke: #e0e0e0;
|
|
60
53
|
}
|
|
61
|
-
|
|
62
54
|
.grid path {
|
|
63
55
|
stroke-width: 0;
|
|
64
56
|
}
|
|
65
|
-
|
|
66
57
|
.no-select {
|
|
67
58
|
user-select: none;
|
|
68
59
|
}
|
|
60
|
+
.selected {
|
|
61
|
+
stroke-width: 2px;
|
|
62
|
+
stroke: var(--ox-gantt-selected-color, blue);
|
|
63
|
+
}
|
|
69
64
|
`
|
|
70
65
|
]
|
|
71
66
|
|
|
72
|
-
@property({ type: String, attribute: 'from-date' })
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@property({ type: String, attribute: 'to-date' })
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
@property({ type: String, attribute: 'from-date' }) fromDate: string = new Date(new Date().getFullYear(), 0, 1)
|
|
68
|
+
.toISOString()
|
|
69
|
+
.split('T')[0]
|
|
70
|
+
@property({ type: String, attribute: 'to-date' }) toDate: string = new Date(new Date().getFullYear(), 11, 31)
|
|
71
|
+
.toISOString()
|
|
72
|
+
.split('T')[0]
|
|
78
73
|
@property({ type: String, attribute: 'axis-scale' }) axisScale: TimeScale = 'week-day'
|
|
79
74
|
@property({ type: Boolean, attribute: 'extend-grid-lines' }) extendGridLines: boolean = false
|
|
80
75
|
@property({ type: Array }) tasks: Task[] = []
|
|
81
|
-
|
|
82
|
-
@
|
|
83
|
-
@state() level: LEVEL = this.axisScale
|
|
84
|
-
|
|
85
|
-
private fromTime: Date = new Date(this.fromDate!)
|
|
86
|
-
private toTime: Date = new Date(this.toDate)
|
|
87
|
-
private totalDays: number = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
|
|
88
|
-
private _timeScale!: d3.ScaleTime<number, number, never>
|
|
76
|
+
@property({ type: String, attribute: 'selected-task-id' }) selectedTaskId?: string
|
|
77
|
+
@property({ type: String }) locale: string = 'en-US'
|
|
89
78
|
|
|
90
79
|
@query('svg[header]') svgHeader!: SVGElement
|
|
91
80
|
@query('svg[body]') svgBody!: SVGElement
|
|
92
81
|
@query('[chart-container]') container!: HTMLDivElement
|
|
82
|
+
@query('ox-task-tooltip') tooltip!: OxTaskTooltip // 툴팁 요소 쿼리 추가
|
|
83
|
+
|
|
84
|
+
private ganttChartController: GanttChartController
|
|
85
|
+
|
|
86
|
+
constructor() {
|
|
87
|
+
super()
|
|
88
|
+
this.ganttChartController = new GanttChartController(this, this.fromDate, this.toDate, this.axisScale)
|
|
89
|
+
}
|
|
93
90
|
|
|
94
91
|
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
|
95
92
|
this.initHeaderGestureHandlers()
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
updated(changedProperties: PropertyValues<this>) {
|
|
99
|
-
var redrawRequired = false
|
|
100
|
-
|
|
101
96
|
if (changedProperties.has('fromDate') || changedProperties.has('toDate')) {
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
|
|
97
|
+
this.ganttChartController.updateRange(this.fromDate, this.toDate)
|
|
98
|
+
}
|
|
105
99
|
|
|
106
|
-
|
|
100
|
+
if (changedProperties.has('tasks')) {
|
|
101
|
+
this.ganttChartController.setTasks(this.tasks)
|
|
102
|
+
this.svgBody.style.height =
|
|
103
|
+
Math.max(this.tasks.length * 30, 2000 /* 태스크가 많이 없어도, 빈 공간이 없도록, 충분히 높게 */) + 'px'
|
|
107
104
|
}
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
// Update locale
|
|
107
|
+
if (changedProperties.has('locale')) {
|
|
108
|
+
this.updateLocale(this.locale)
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
if (changedProperties.has('
|
|
114
|
-
this.
|
|
115
|
-
|
|
111
|
+
if (changedProperties.has('selectedTaskId')) {
|
|
112
|
+
this.ganttChartController.selectedTaskId = this.selectedTaskId
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (changedProperties.has('extendGridLines')) {
|
|
116
|
+
this.ganttChartController.extendGridLines = this.extendGridLines
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
this.ganttChartController.drawAxes(this.svgHeader)
|
|
120
|
+
this.ganttChartController.drawBody(this.svgBody, this.tooltip)
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
render() {
|
|
122
124
|
return html`
|
|
123
|
-
<div
|
|
125
|
+
<div
|
|
126
|
+
chart-container
|
|
127
|
+
@wheel=${(e: WheelEvent) => this.ganttChartController.handleWheelForBody(e, this.container)}
|
|
128
|
+
@touchstart=${(e: TouchEvent) => this.ganttChartController.handleTouchStartForBody(e, this.container)}
|
|
129
|
+
@touchmove=${(e: TouchEvent) => this.ganttChartController.handleTouchMoveForBody(e, this.container)}
|
|
130
|
+
>
|
|
124
131
|
<svg
|
|
125
132
|
header
|
|
126
|
-
@date-range-selected=${(e: CustomEvent) =>
|
|
127
|
-
console.log(e.detail)
|
|
128
|
-
}}
|
|
133
|
+
@date-range-selected=${(e: CustomEvent) => console.log(e.detail)}
|
|
129
134
|
@wheel=${this.handleWheelForHeader.bind(this)}
|
|
130
135
|
></svg>
|
|
131
136
|
<svg body></svg>
|
|
137
|
+
<ox-task-tooltip></ox-task-tooltip> // 툴팁 요소 추가
|
|
132
138
|
</div>
|
|
133
139
|
`
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
setVerticalOffset(offset: number) {
|
|
137
|
-
console.log(offset)
|
|
138
143
|
this.container.scrollTop = offset
|
|
139
144
|
}
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return 7 / 2
|
|
144
|
-
} else if (this.level == 'year-month') {
|
|
145
|
-
return 30 / 2
|
|
146
|
-
} else if (this.level == 'year-quarter') {
|
|
147
|
-
return 90 / 2
|
|
148
|
-
}
|
|
149
|
-
return 1
|
|
146
|
+
getVerticalOffset(): number {
|
|
147
|
+
return this.container.scrollTop
|
|
150
148
|
}
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
get timeScale() {
|
|
157
|
-
if (!this._timeScale) {
|
|
158
|
-
// 시간축 스케일 생성
|
|
159
|
-
this._timeScale = d3
|
|
160
|
-
.scaleTime()
|
|
161
|
-
.domain([d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1)])
|
|
162
|
-
.range([0, this.getChartWidth()])
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return this._timeScale
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
updateTimeScale() {
|
|
169
|
-
this.timeScale.range([0, this.getChartWidth()])
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
redraw() {
|
|
173
|
-
this.drawAxes()
|
|
174
|
-
this.drawBody()
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
drawBody() {
|
|
178
|
-
const body = d3.select(this.svgBody)
|
|
179
|
-
|
|
180
|
-
body.selectAll('*').remove()
|
|
181
|
-
body.attr('width', this.getChartWidth())
|
|
182
|
-
|
|
183
|
-
if (this.extendGridLines) {
|
|
184
|
-
const addGridLines = (ticks: Date[]) => {
|
|
185
|
-
ticks.forEach(tick => {
|
|
186
|
-
const startX = this.timeScale(tick)
|
|
187
|
-
body
|
|
188
|
-
.append('line')
|
|
189
|
-
.attr('x1', startX)
|
|
190
|
-
.attr('x2', startX)
|
|
191
|
-
.attr('y1', 0)
|
|
192
|
-
.attr('y2', Number(this.svgBody.getBoundingClientRect().height) || 0)
|
|
193
|
-
.attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
|
|
194
|
-
.attr('stroke-width', 1)
|
|
195
|
-
})
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (this.level === 'week-day') {
|
|
199
|
-
const dayTicks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
|
|
200
|
-
addGridLines(dayTicks)
|
|
201
|
-
} else if (this.level === 'quarter-week') {
|
|
202
|
-
const weekTicks = d3.timeWeek
|
|
203
|
-
.every(1)!
|
|
204
|
-
.range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
|
|
205
|
-
addGridLines(weekTicks)
|
|
206
|
-
} else if (this.level === 'year-month') {
|
|
207
|
-
const monthTicks = d3.timeMonth
|
|
208
|
-
.every(1)!
|
|
209
|
-
.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
|
|
210
|
-
addGridLines(monthTicks)
|
|
211
|
-
} else if (this.level === 'year-quarter') {
|
|
212
|
-
const quarterTicks = d3.timeMonth
|
|
213
|
-
.every(3)!
|
|
214
|
-
.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
|
|
215
|
-
addGridLines(quarterTicks)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
150
|
+
handleWheelForHeader(event: WheelEvent) {
|
|
151
|
+
event.preventDefault()
|
|
152
|
+
event.stopPropagation()
|
|
218
153
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.tasks.forEach((task, index) => {
|
|
223
|
-
const startX = this.timeScale(d3.timeDay.floor(d3.min([task.startTime, task.endTime])!))
|
|
224
|
-
const endX = this.timeScale(d3.timeDay.offset(d3.timeDay.ceil(d3.max([task.startTime, task.endTime])!), -1))
|
|
225
|
-
|
|
226
|
-
const taskWidth = endX - startX + this.baseWidth / this.getScale()
|
|
227
|
-
const taskY = index * 30 + 7.5
|
|
228
|
-
|
|
229
|
-
// Main task bar
|
|
230
|
-
const taskBar = body
|
|
231
|
-
.append('rect')
|
|
232
|
-
.attr('x', startX)
|
|
233
|
-
.attr('y', taskY)
|
|
234
|
-
.attr('width', taskWidth)
|
|
235
|
-
.attr('height', 15)
|
|
236
|
-
.attr('fill', this.getTaskColor(task.tags || []))
|
|
237
|
-
.attr('stroke', 'black')
|
|
238
|
-
.attr('stroke-width', 1)
|
|
239
|
-
|
|
240
|
-
// Add click event listener to task bar
|
|
241
|
-
taskBar.on('click', () => {
|
|
242
|
-
const customEvent = new CustomEvent('task-clicked', {
|
|
243
|
-
detail: { task },
|
|
244
|
-
bubbles: true,
|
|
245
|
-
composed: true
|
|
246
|
-
})
|
|
247
|
-
this.dispatchEvent(customEvent)
|
|
154
|
+
if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
|
|
155
|
+
this.container.scrollBy({
|
|
156
|
+
left: event.shiftKey ? event.deltaX : event.deltaY
|
|
248
157
|
})
|
|
158
|
+
} else {
|
|
159
|
+
const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9
|
|
160
|
+
this.ganttChartController.baseWidth *= zoomFactor
|
|
249
161
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.
|
|
255
|
-
.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
.
|
|
162
|
+
if (this.ganttChartController.baseWidth > 40) {
|
|
163
|
+
if (this.ganttChartController.level == 'week-day') {
|
|
164
|
+
this.ganttChartController.baseWidth = 40
|
|
165
|
+
} else {
|
|
166
|
+
this.ganttChartController.level = levelDown(this.ganttChartController.level)
|
|
167
|
+
this.ganttChartController.baseWidth = 30
|
|
168
|
+
}
|
|
169
|
+
} else if (this.ganttChartController.baseWidth < 30) {
|
|
170
|
+
if (this.ganttChartController.level == 'year-quarter') {
|
|
171
|
+
this.ganttChartController.baseWidth = 30
|
|
172
|
+
}
|
|
260
173
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
// Add linking lines
|
|
267
|
-
this.tasks.forEach(task => {
|
|
268
|
-
if (task.after && taskPositions[task.after]) {
|
|
269
|
-
const prevTask = taskPositions[task.after]
|
|
270
|
-
const currTask = taskPositions[task.id]
|
|
271
|
-
|
|
272
|
-
// Draw the linking line
|
|
273
|
-
body
|
|
274
|
-
.append('path')
|
|
275
|
-
.attr(
|
|
276
|
-
'd',
|
|
277
|
-
`
|
|
278
|
-
M${prevTask.x},${prevTask.y}
|
|
279
|
-
L${prevTask.x + 10},${prevTask.y}
|
|
280
|
-
${
|
|
281
|
-
prevTask.x + 10 < currTask.x - 10 - currTask.width
|
|
282
|
-
? `
|
|
283
|
-
L${(prevTask.x + currTask.x - currTask.width) / 2},${prevTask.y}
|
|
284
|
-
L${(prevTask.x + currTask.x - currTask.width) / 2},${currTask.y}
|
|
285
|
-
`
|
|
286
|
-
: `
|
|
287
|
-
L${prevTask.x + 10},${(prevTask.y + currTask.y) / 2}
|
|
288
|
-
L${currTask.x - 10 - currTask.width},${(prevTask.y + currTask.y) / 2}
|
|
289
|
-
`
|
|
290
|
-
}
|
|
291
|
-
L${currTask.x - 10 - currTask.width},${currTask.y}
|
|
292
|
-
L${currTask.x - currTask.width},${currTask.y}
|
|
293
|
-
`
|
|
294
|
-
)
|
|
295
|
-
.attr('stroke', 'black')
|
|
296
|
-
.attr('fill', 'none')
|
|
297
|
-
.attr('stroke-width', 1)
|
|
174
|
+
if (this.ganttChartController.baseWidth < 20) {
|
|
175
|
+
this.ganttChartController.level = levelUp(this.ganttChartController.level)
|
|
176
|
+
this.ganttChartController.baseWidth = 30
|
|
298
177
|
}
|
|
299
|
-
})
|
|
300
|
-
}
|
|
301
178
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (tags.includes('critical')) {
|
|
305
|
-
return 'red'
|
|
306
|
-
} else if (tags.includes('milestone')) {
|
|
307
|
-
return 'blue'
|
|
308
|
-
} else if (tags.includes('done')) {
|
|
309
|
-
return 'green'
|
|
310
|
-
} else {
|
|
311
|
-
return 'grey'
|
|
179
|
+
this.ganttChartController.updateTimeScale()
|
|
180
|
+
this.requestUpdate()
|
|
312
181
|
}
|
|
313
182
|
}
|
|
314
183
|
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
// 차트 컨테이너의 폭 설정 및 스크롤 가능하도록 설정
|
|
322
|
-
header.attr('width', this.getChartWidth())
|
|
323
|
-
|
|
324
|
-
const addGridLines = (ticks: Date[], yStart: number, yEnd: number, extendable?: boolean) => {
|
|
325
|
-
ticks.forEach(tick => {
|
|
326
|
-
const startX = this.timeScale(tick)
|
|
327
|
-
header
|
|
328
|
-
.append('line')
|
|
329
|
-
.attr('x1', startX)
|
|
330
|
-
.attr('x2', startX)
|
|
331
|
-
.attr('y1', yStart)
|
|
332
|
-
.attr('y2', yEnd)
|
|
333
|
-
.attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
|
|
334
|
-
.attr('stroke-width', 1)
|
|
335
|
-
})
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const centerLabel = (
|
|
339
|
-
selection: d3.Selection<SVGGElement, unknown, null, any>,
|
|
340
|
-
tickInterval: d3.CountableTimeInterval
|
|
341
|
-
) => {
|
|
342
|
-
selection.selectAll('text').attr('transform', (d: any) => {
|
|
343
|
-
const tickIntervalWidth = this.timeScale(tickInterval.offset(d, 1)) - this.timeScale(d)
|
|
344
|
-
return `translate(${tickIntervalWidth / 2}, -2)`
|
|
345
|
-
})
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (this.level === 'week-day') {
|
|
349
|
-
// Level 1: week-day
|
|
350
|
-
// Weekly axis
|
|
351
|
-
const weekAxis = d3
|
|
352
|
-
.axisTop(this.timeScale)
|
|
353
|
-
.ticks(d3.timeWeek.every(1))
|
|
354
|
-
.tickFormat(d3.timeFormat('%B %d') as any)
|
|
355
|
-
.tickSize(0)
|
|
356
|
-
const weekTicks = d3.timeWeek
|
|
357
|
-
.every(1)!
|
|
358
|
-
.range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
|
|
359
|
-
addGridLines(weekTicks, 0, 20)
|
|
360
|
-
|
|
361
|
-
const weekAxisG = header
|
|
362
|
-
.append('g')
|
|
363
|
-
.classed('week-axis no-select md-typescale-label-small', true)
|
|
364
|
-
.attr('transform', `translate(0, 20)`)
|
|
365
|
-
.call(weekAxis)
|
|
366
|
-
|
|
367
|
-
centerLabel(weekAxisG, d3.timeWeek)
|
|
368
|
-
|
|
369
|
-
// Daily axis
|
|
370
|
-
const dayAxis = d3
|
|
371
|
-
.axisTop(this.timeScale)
|
|
372
|
-
.ticks(d3.timeDay.every(1))
|
|
373
|
-
.tickFormat(d3.timeFormat('%d') as any)
|
|
374
|
-
.tickSize(0)
|
|
375
|
-
|
|
376
|
-
const dayTicks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
|
|
377
|
-
addGridLines(dayTicks, 20, 40)
|
|
378
|
-
|
|
379
|
-
const dayAxisG = header
|
|
380
|
-
.append('g')
|
|
381
|
-
.classed('day-axis no-select md-typescale-label-small', true)
|
|
382
|
-
.attr('transform', `translate(0, 40)`)
|
|
383
|
-
.call(dayAxis)
|
|
384
|
-
|
|
385
|
-
centerLabel(dayAxisG, d3.timeDay)
|
|
386
|
-
} else if (this.level === 'quarter-week') {
|
|
387
|
-
// Level 2: quarter-week
|
|
388
|
-
// Quarterly axis
|
|
389
|
-
const quarterAxis = d3
|
|
390
|
-
.axisTop(this.timeScale)
|
|
391
|
-
.ticks(d3.timeMonth.every(3))
|
|
392
|
-
.tickFormat(d3.timeFormat('%Y %b') as any)
|
|
393
|
-
.tickSize(0)
|
|
394
|
-
|
|
395
|
-
const quarterTicks = d3.timeMonth
|
|
396
|
-
.every(3)!
|
|
397
|
-
.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
|
|
398
|
-
addGridLines(quarterTicks, 0, 20)
|
|
399
|
-
|
|
400
|
-
const quarterAxisG = header
|
|
401
|
-
.append('g')
|
|
402
|
-
.classed('quarter-axis no-select md-typescale-label-small', true)
|
|
403
|
-
.attr('transform', `translate(0, 20)`)
|
|
404
|
-
.call(quarterAxis)
|
|
405
|
-
|
|
406
|
-
centerLabel(quarterAxisG, d3.timeMonth.every(3) as any)
|
|
407
|
-
|
|
408
|
-
// Weekly axis
|
|
409
|
-
const weekAxis = d3
|
|
410
|
-
.axisTop(this.timeScale)
|
|
411
|
-
.ticks(d3.timeWeek.every(1))
|
|
412
|
-
.tickFormat(d3.timeFormat('%b %d') as any)
|
|
413
|
-
.tickSize(0)
|
|
414
|
-
const weekTicks = d3.timeWeek
|
|
415
|
-
.every(1)!
|
|
416
|
-
.range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
|
|
417
|
-
addGridLines(weekTicks, 20, 40)
|
|
418
|
-
|
|
419
|
-
const weekAxisG = header
|
|
420
|
-
.append('g')
|
|
421
|
-
.classed('week-axis no-select md-typescale-label-small', true)
|
|
422
|
-
.attr('transform', `translate(0, 40)`)
|
|
423
|
-
.call(weekAxis)
|
|
424
|
-
|
|
425
|
-
centerLabel(weekAxisG, d3.timeWeek)
|
|
426
|
-
} else if (this.level === 'year-month') {
|
|
427
|
-
// Level 3: year-month
|
|
428
|
-
// Yearly axis
|
|
429
|
-
const yearAxis = d3
|
|
430
|
-
.axisTop(this.timeScale)
|
|
431
|
-
.ticks(d3.timeYear.every(1))
|
|
432
|
-
.tickFormat(d3.timeFormat('%Y') as any)
|
|
433
|
-
.tickSize(0)
|
|
434
|
-
|
|
435
|
-
const yearTicks = d3.timeYear
|
|
436
|
-
.every(1)!
|
|
437
|
-
.range(d3.timeYear.floor(this.fromTime), d3.timeYear.offset(this.toTime, 1))
|
|
438
|
-
addGridLines(yearTicks, 0, 20)
|
|
439
|
-
|
|
440
|
-
const yearAxisG = header
|
|
441
|
-
.append('g')
|
|
442
|
-
.classed('year-axis no-select md-typescale-label-small', true)
|
|
443
|
-
.attr('transform', `translate(0, 20)`)
|
|
444
|
-
.call(yearAxis)
|
|
445
|
-
|
|
446
|
-
centerLabel(yearAxisG, d3.timeYear.every(1)! as any)
|
|
447
|
-
|
|
448
|
-
// Monthly axis
|
|
449
|
-
const monthAxis = d3
|
|
450
|
-
.axisTop(this.timeScale)
|
|
451
|
-
.ticks(d3.timeMonth.every(1))
|
|
452
|
-
.tickFormat(d3.timeFormat('%b') as any)
|
|
453
|
-
.tickSize(0)
|
|
454
|
-
|
|
455
|
-
const monthTicks = d3.timeMonth
|
|
456
|
-
.every(1)!
|
|
457
|
-
.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
|
|
458
|
-
addGridLines(monthTicks, 20, 40)
|
|
459
|
-
|
|
460
|
-
const monthAxisG = header
|
|
461
|
-
.append('g')
|
|
462
|
-
.classed('month-axis no-select md-typescale-label-small', true)
|
|
463
|
-
.attr('transform', `translate(0, 40)`)
|
|
464
|
-
.call(monthAxis)
|
|
465
|
-
|
|
466
|
-
centerLabel(monthAxisG, d3.timeMonth.every(1) as any)
|
|
467
|
-
} else if (this.level === 'year-quarter') {
|
|
468
|
-
// Level 4: year-quarter
|
|
469
|
-
// Yearly axis
|
|
470
|
-
const yearAxis = d3
|
|
471
|
-
.axisTop(this.timeScale)
|
|
472
|
-
.ticks(d3.timeYear.every(1))
|
|
473
|
-
.tickFormat(d3.timeFormat('%Y') as any)
|
|
474
|
-
.tickSize(0)
|
|
475
|
-
|
|
476
|
-
const yearTicks = d3.timeYear
|
|
477
|
-
.every(1)!
|
|
478
|
-
.range(d3.timeYear.floor(this.fromTime), d3.timeYear.offset(this.toTime, 1))
|
|
479
|
-
addGridLines(yearTicks, 0, 20)
|
|
480
|
-
|
|
481
|
-
const yearAxisG = header
|
|
482
|
-
.append('g')
|
|
483
|
-
.classed('year-axis no-select md-typescale-label-small', true)
|
|
484
|
-
.attr('transform', `translate(0, 20)`)
|
|
485
|
-
.call(yearAxis)
|
|
486
|
-
|
|
487
|
-
centerLabel(yearAxisG, d3.timeYear.every(1)! as any)
|
|
488
|
-
|
|
489
|
-
// Quarterly axis
|
|
490
|
-
const quarterAxis = d3
|
|
491
|
-
.axisTop(this.timeScale)
|
|
492
|
-
.ticks(d3.timeMonth.every(3))
|
|
493
|
-
.tickFormat(d3.timeFormat('%Y %b') as any)
|
|
494
|
-
.tickSize(0)
|
|
495
|
-
|
|
496
|
-
const quarterTicks = d3.timeMonth
|
|
497
|
-
.every(3)!
|
|
498
|
-
.range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
|
|
499
|
-
addGridLines(quarterTicks, 20, 40)
|
|
500
|
-
|
|
501
|
-
const quarterAxisG = header
|
|
502
|
-
.append('g')
|
|
503
|
-
.classed('quarter-axis no-select md-typescale-label-small', true)
|
|
504
|
-
.attr('transform', `translate(0, 40)`)
|
|
505
|
-
.call(quarterAxis)
|
|
506
|
-
|
|
507
|
-
centerLabel(quarterAxisG, d3.timeMonth.every(3) as any)
|
|
184
|
+
updateLocale(locale: string) {
|
|
185
|
+
const localeDefinition = (locales as any)[locale || 'en-US']
|
|
186
|
+
if (localeDefinition) {
|
|
187
|
+
d3.timeFormatDefaultLocale(localeDefinition)
|
|
188
|
+
this.ganttChartController.updateTimeScale()
|
|
508
189
|
}
|
|
509
190
|
}
|
|
510
191
|
|
|
@@ -516,7 +197,7 @@ export class OxGanttChart extends LitElement {
|
|
|
516
197
|
let lastTouchDistance: number | null = null
|
|
517
198
|
|
|
518
199
|
const handleTouchStart = (event: TouchEvent) => {
|
|
519
|
-
event.preventDefault()
|
|
200
|
+
event.preventDefault()
|
|
520
201
|
|
|
521
202
|
if (event.touches.length === 2) {
|
|
522
203
|
lastTouchDistance = getTouchDistance(event.touches)
|
|
@@ -529,32 +210,32 @@ export class OxGanttChart extends LitElement {
|
|
|
529
210
|
}
|
|
530
211
|
|
|
531
212
|
const handleTouchMove = (event: TouchEvent) => {
|
|
532
|
-
event.preventDefault()
|
|
213
|
+
event.preventDefault()
|
|
533
214
|
|
|
534
215
|
if (event.touches.length === 2 && lastTouchDistance !== null) {
|
|
535
216
|
const currentDistance = getTouchDistance(event.touches)
|
|
536
217
|
const zoomFactor = currentDistance / lastTouchDistance
|
|
537
|
-
this.baseWidth *= zoomFactor
|
|
218
|
+
this.ganttChartController.baseWidth *= zoomFactor
|
|
538
219
|
|
|
539
|
-
if (this.baseWidth > 60) {
|
|
540
|
-
if (this.level == 'year-quarter') {
|
|
541
|
-
this.baseWidth = 60
|
|
220
|
+
if (this.ganttChartController.baseWidth > 60) {
|
|
221
|
+
if (this.ganttChartController.level == 'year-quarter') {
|
|
222
|
+
this.ganttChartController.baseWidth = 60
|
|
542
223
|
} else {
|
|
543
|
-
this.level = levelUp(this.level)
|
|
544
|
-
this.baseWidth = 30
|
|
224
|
+
this.ganttChartController.level = levelUp(this.ganttChartController.level)
|
|
225
|
+
this.ganttChartController.baseWidth = 30
|
|
545
226
|
}
|
|
546
|
-
} else if (this.baseWidth < 30) {
|
|
547
|
-
if (this.level == 'week-day') {
|
|
548
|
-
this.baseWidth = 30
|
|
227
|
+
} else if (this.ganttChartController.baseWidth < 30) {
|
|
228
|
+
if (this.ganttChartController.level == 'week-day') {
|
|
229
|
+
this.ganttChartController.baseWidth = 30
|
|
549
230
|
} else {
|
|
550
|
-
this.level = levelDown(this.level)
|
|
551
|
-
this.baseWidth = 60
|
|
231
|
+
this.ganttChartController.level = levelDown(this.ganttChartController.level)
|
|
232
|
+
this.ganttChartController.baseWidth = 60
|
|
552
233
|
}
|
|
553
234
|
}
|
|
554
235
|
|
|
555
236
|
lastTouchDistance = currentDistance
|
|
556
|
-
this.updateTimeScale()
|
|
557
|
-
this.
|
|
237
|
+
this.ganttChartController.updateTimeScale()
|
|
238
|
+
this.requestUpdate()
|
|
558
239
|
} else if (isDragging && startX !== null) {
|
|
559
240
|
const touch = event.touches[0]
|
|
560
241
|
const containerOffsetX = header.node()!.getBoundingClientRect().left
|
|
@@ -564,14 +245,13 @@ export class OxGanttChart extends LitElement {
|
|
|
564
245
|
}
|
|
565
246
|
|
|
566
247
|
const handleTouchEnd = (event: TouchEvent) => {
|
|
567
|
-
event.preventDefault()
|
|
248
|
+
event.preventDefault()
|
|
568
249
|
|
|
569
250
|
if (event.touches.length < 2) {
|
|
570
251
|
lastTouchDistance = null
|
|
571
252
|
}
|
|
572
253
|
|
|
573
254
|
if (!isDragging || startX === null) {
|
|
574
|
-
// 클릭 이벤트 처리
|
|
575
255
|
const touch = event.changedTouches[0]
|
|
576
256
|
const containerOffsetX = header.node()!.getBoundingClientRect().left
|
|
577
257
|
const clickX = touch.clientX - containerOffsetX
|
|
@@ -610,7 +290,6 @@ export class OxGanttChart extends LitElement {
|
|
|
610
290
|
|
|
611
291
|
const handleMouseUp = (event: MouseEvent) => {
|
|
612
292
|
if (!isDragging || startX === null) {
|
|
613
|
-
// 클릭 이벤트 처리
|
|
614
293
|
const containerOffsetX = header.node()!.getBoundingClientRect().left
|
|
615
294
|
const clickX = event.clientX - containerOffsetX
|
|
616
295
|
this.finalizeSelection(clickX, clickX)
|
|
@@ -637,21 +316,21 @@ export class OxGanttChart extends LitElement {
|
|
|
637
316
|
const header = d3.select(this.svgHeader)
|
|
638
317
|
const body = d3.select(this.svgBody)
|
|
639
318
|
|
|
640
|
-
let startDate = this.timeScale.invert(startX)
|
|
641
|
-
let endDate = this.timeScale.invert(endX)
|
|
319
|
+
let startDate = this.ganttChartController.timeScale.invert(startX)
|
|
320
|
+
let endDate = this.ganttChartController.timeScale.invert(endX)
|
|
642
321
|
|
|
643
322
|
let start, end
|
|
644
323
|
|
|
645
|
-
if (this.level === 'week-day') {
|
|
324
|
+
if (this.ganttChartController.level === 'week-day') {
|
|
646
325
|
start = d3.timeDay.floor(d3.min([startDate, endDate])!)
|
|
647
326
|
end = d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1)
|
|
648
|
-
} else if (this.level === 'quarter-week') {
|
|
327
|
+
} else if (this.ganttChartController.level === 'quarter-week') {
|
|
649
328
|
start = d3.timeWeek.floor(d3.min([startDate, endDate])!)
|
|
650
329
|
end = d3.timeDay.offset(d3.timeWeek.ceil(d3.max([startDate, endDate])!), -1)
|
|
651
|
-
} else if (this.level === 'year-month') {
|
|
330
|
+
} else if (this.ganttChartController.level === 'year-month') {
|
|
652
331
|
start = d3.timeMonth.floor(d3.min([startDate, endDate])!)
|
|
653
332
|
end = d3.timeDay.offset(d3.timeMonth.ceil(d3.max([startDate, endDate])!), -1)
|
|
654
|
-
} else if (this.level === 'year-quarter') {
|
|
333
|
+
} else if (this.ganttChartController.level === 'year-quarter') {
|
|
655
334
|
start = d3.timeMonth.every(3)!.floor(d3.min([startDate, endDate])!)
|
|
656
335
|
end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
|
|
657
336
|
}
|
|
@@ -662,9 +341,14 @@ export class OxGanttChart extends LitElement {
|
|
|
662
341
|
header
|
|
663
342
|
.append('rect')
|
|
664
343
|
.attr('class', 'selection-rect')
|
|
665
|
-
.attr('x', this.timeScale(start!))
|
|
344
|
+
.attr('x', this.ganttChartController.timeScale(start!))
|
|
666
345
|
.attr('y', 20)
|
|
667
|
-
.attr(
|
|
346
|
+
.attr(
|
|
347
|
+
'width',
|
|
348
|
+
this.ganttChartController.timeScale(end!) -
|
|
349
|
+
this.ganttChartController.timeScale(start!) +
|
|
350
|
+
this.ganttChartController.baseWidth / this.ganttChartController.getScale()
|
|
351
|
+
)
|
|
668
352
|
.attr('height', 20)
|
|
669
353
|
.attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
|
|
670
354
|
.attr('opacity', '.2')
|
|
@@ -672,9 +356,14 @@ export class OxGanttChart extends LitElement {
|
|
|
672
356
|
body
|
|
673
357
|
.append('rect')
|
|
674
358
|
.attr('class', 'selection-rect')
|
|
675
|
-
.attr('x', this.timeScale(start!))
|
|
359
|
+
.attr('x', this.ganttChartController.timeScale(start!))
|
|
676
360
|
.attr('y', 0)
|
|
677
|
-
.attr(
|
|
361
|
+
.attr(
|
|
362
|
+
'width',
|
|
363
|
+
this.ganttChartController.timeScale(end!) -
|
|
364
|
+
this.ganttChartController.timeScale(start!) +
|
|
365
|
+
this.ganttChartController.baseWidth / this.ganttChartController.getScale()
|
|
366
|
+
)
|
|
678
367
|
.attr('height', '100%')
|
|
679
368
|
.attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
|
|
680
369
|
.attr('opacity', '.2')
|
|
@@ -684,21 +373,21 @@ export class OxGanttChart extends LitElement {
|
|
|
684
373
|
const header = d3.select(this.svgHeader)
|
|
685
374
|
const body = d3.select(this.svgBody)
|
|
686
375
|
|
|
687
|
-
let startDate = this.timeScale.invert(startX)
|
|
688
|
-
let endDate = this.timeScale.invert(endX)
|
|
376
|
+
let startDate = this.ganttChartController.timeScale.invert(startX)
|
|
377
|
+
let endDate = this.ganttChartController.timeScale.invert(endX)
|
|
689
378
|
|
|
690
379
|
let start, end
|
|
691
380
|
|
|
692
|
-
if (this.level === 'week-day') {
|
|
381
|
+
if (this.ganttChartController.level === 'week-day') {
|
|
693
382
|
start = d3.timeDay.floor(d3.min([startDate, endDate])!)
|
|
694
383
|
end = d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1)
|
|
695
|
-
} else if (this.level === 'quarter-week') {
|
|
384
|
+
} else if (this.ganttChartController.level === 'quarter-week') {
|
|
696
385
|
start = d3.timeWeek.floor(d3.min([startDate, endDate])!)
|
|
697
386
|
end = d3.timeDay.offset(d3.timeWeek.ceil(d3.max([startDate, endDate])!), -1)
|
|
698
|
-
} else if (this.level === 'year-month') {
|
|
387
|
+
} else if (this.ganttChartController.level === 'year-month') {
|
|
699
388
|
start = d3.timeMonth.floor(d3.min([startDate, endDate])!)
|
|
700
389
|
end = d3.timeDay.offset(d3.timeMonth.ceil(d3.max([startDate, endDate])!), -1)
|
|
701
|
-
} else if (this.level === 'year-quarter') {
|
|
390
|
+
} else if (this.ganttChartController.level === 'year-quarter') {
|
|
702
391
|
start = d3.timeMonth.every(3)!.floor(d3.min([startDate, endDate])!)
|
|
703
392
|
end = d3.timeDay.offset(d3.timeMonth.every(3)!.ceil(d3.max([startDate, endDate])!), -1)
|
|
704
393
|
}
|
|
@@ -709,9 +398,14 @@ export class OxGanttChart extends LitElement {
|
|
|
709
398
|
header
|
|
710
399
|
.append('rect')
|
|
711
400
|
.attr('class', 'selection-rect')
|
|
712
|
-
.attr('x', this.timeScale(start!))
|
|
401
|
+
.attr('x', this.ganttChartController.timeScale(start!))
|
|
713
402
|
.attr('y', 20)
|
|
714
|
-
.attr(
|
|
403
|
+
.attr(
|
|
404
|
+
'width',
|
|
405
|
+
this.ganttChartController.timeScale(end!) -
|
|
406
|
+
this.ganttChartController.timeScale(start!) +
|
|
407
|
+
this.ganttChartController.baseWidth / this.ganttChartController.getScale()
|
|
408
|
+
)
|
|
715
409
|
.attr('height', 20)
|
|
716
410
|
.attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
|
|
717
411
|
.attr('opacity', '.2')
|
|
@@ -719,9 +413,14 @@ export class OxGanttChart extends LitElement {
|
|
|
719
413
|
body
|
|
720
414
|
.append('rect')
|
|
721
415
|
.attr('class', 'selection-rect')
|
|
722
|
-
.attr('x', this.timeScale(start!))
|
|
416
|
+
.attr('x', this.ganttChartController.timeScale(start!))
|
|
723
417
|
.attr('y', 0)
|
|
724
|
-
.attr(
|
|
418
|
+
.attr(
|
|
419
|
+
'width',
|
|
420
|
+
this.ganttChartController.timeScale(end!) -
|
|
421
|
+
this.ganttChartController.timeScale(start!) +
|
|
422
|
+
this.ganttChartController.baseWidth / this.ganttChartController.getScale()
|
|
423
|
+
)
|
|
725
424
|
.attr('height', '100%')
|
|
726
425
|
.attr('fill', 'var(--ox-gantt-selection-color, var(--md-sys-color-secondary))')
|
|
727
426
|
.attr('opacity', '.2')
|
|
@@ -733,41 +432,4 @@ export class OxGanttChart extends LitElement {
|
|
|
733
432
|
|
|
734
433
|
header.node()?.dispatchEvent(customEvent)
|
|
735
434
|
}
|
|
736
|
-
|
|
737
|
-
handleWheelForHeader(event: WheelEvent) {
|
|
738
|
-
event.preventDefault()
|
|
739
|
-
event.stopPropagation()
|
|
740
|
-
|
|
741
|
-
if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
|
|
742
|
-
// 좌우 스크롤
|
|
743
|
-
this.container.scrollBy({
|
|
744
|
-
left: event.shiftKey ? event.deltaX : event.deltaY,
|
|
745
|
-
behavior: 'smooth'
|
|
746
|
-
})
|
|
747
|
-
} else {
|
|
748
|
-
// Zoom in/out
|
|
749
|
-
const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9
|
|
750
|
-
this.baseWidth *= zoomFactor
|
|
751
|
-
|
|
752
|
-
if (this.baseWidth > 40) {
|
|
753
|
-
if (this.level == 'week-day') {
|
|
754
|
-
this.baseWidth = 40
|
|
755
|
-
} else {
|
|
756
|
-
this.level = levelDown(this.level)
|
|
757
|
-
this.baseWidth = 30
|
|
758
|
-
}
|
|
759
|
-
} else if (this.baseWidth < 30) {
|
|
760
|
-
if (this.level == 'year-quarter') {
|
|
761
|
-
this.baseWidth = 30
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
if (this.baseWidth < 20) {
|
|
765
|
-
this.level = levelUp(this.level)
|
|
766
|
-
this.baseWidth = 30
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
this.updateTimeScale()
|
|
770
|
-
this.redraw()
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
435
|
}
|