@operato/gantt 2.0.0-beta.4 → 7.0.0-rc.0

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 (47) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/src/gantt-chart-controller.d.ts +32 -0
  3. package/dist/src/gantt-chart-controller.js +395 -0
  4. package/dist/src/gantt-chart-controller.js.map +1 -0
  5. package/dist/src/locales.d.ts +52 -0
  6. package/dist/src/locales.js +79 -0
  7. package/dist/src/locales.js.map +1 -0
  8. package/dist/src/ox-gantt-chart.d.ts +11 -20
  9. package/dist/src/ox-gantt-chart.js +141 -435
  10. package/dist/src/ox-gantt-chart.js.map +1 -1
  11. package/dist/src/ox-gantt-resize-splitter.d.ts +2 -3
  12. package/dist/src/ox-gantt-resize-splitter.js +15 -22
  13. package/dist/src/ox-gantt-resize-splitter.js.map +1 -1
  14. package/dist/src/ox-gantt.d.ts +17 -3
  15. package/dist/src/ox-gantt.js +169 -29
  16. package/dist/src/ox-gantt.js.map +1 -1
  17. package/dist/src/ox-task-tooltip.d.ts +12 -0
  18. package/dist/src/ox-task-tooltip.js +132 -0
  19. package/dist/src/ox-task-tooltip.js.map +1 -0
  20. package/dist/src/types.d.ts +11 -4
  21. package/dist/src/types.js.map +1 -1
  22. package/dist/src/utils.d.ts +3 -3
  23. package/dist/src/utils.js.map +1 -1
  24. package/dist/stories/ox-gantt.stories.d.ts +19 -2
  25. package/dist/stories/ox-gantt.stories.js +104 -90
  26. package/dist/stories/ox-gantt.stories.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +7 -4
  29. package/src/gantt-chart-controller.ts +473 -0
  30. package/src/locales.ts +78 -0
  31. package/src/ox-gantt-chart.ts +151 -484
  32. package/src/ox-gantt-resize-splitter.ts +18 -25
  33. package/src/ox-gantt.ts +174 -31
  34. package/src/ox-task-tooltip.ts +120 -0
  35. package/src/types.ts +13 -5
  36. package/src/utils.ts +4 -4
  37. package/stories/ox-gantt.stories.ts +118 -91
  38. package/themes/grist-theme.css +5 -3
  39. package/translations/en.json +11 -1
  40. package/translations/ja.json +11 -1
  41. package/translations/ko.json +11 -1
  42. package/translations/ms.json +11 -1
  43. package/translations/zh.json +11 -1
  44. package/dist/src/locale.d.ts +0 -1
  45. package/dist/src/locale.js +0 -49
  46. package/dist/src/locale.js.map +0 -1
  47. package/src/locale.ts +0 -51
@@ -0,0 +1,473 @@
1
+ import { LitElement, ReactiveController, ReactiveControllerHost } from 'lit'
2
+ import * as d3 from 'd3'
3
+ import { TimeScale, Task, TaskTag } from './types'
4
+ import { OxTaskTooltip } from './ox-task-tooltip'
5
+
6
+ export class GanttChartController implements ReactiveController {
7
+ private host: ReactiveControllerHost & LitElement
8
+ private fromTime: Date
9
+ private toTime: Date
10
+ private totalDays: number
11
+ private _timeScale?: d3.ScaleTime<number, number, never>
12
+
13
+ private touchStartY?: number
14
+ private scrollStartY?: number
15
+
16
+ public baseWidth: number = 30
17
+ public level: TimeScale
18
+ public selectedTaskId?: string
19
+
20
+ public tasks: Task[] = []
21
+ public extendGridLines: boolean = false
22
+
23
+ constructor(host: ReactiveControllerHost & LitElement, fromDate: string, toDate: string, axisScale: TimeScale) {
24
+ this.host = host
25
+ this.fromTime = new Date(fromDate)
26
+ this.toTime = new Date(toDate)
27
+ this.level = axisScale
28
+ this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
29
+ this.host.addController(this)
30
+ }
31
+
32
+ hostConnected() {
33
+ this.updateTimeScale()
34
+ }
35
+
36
+ get timeScale() {
37
+ if (!this._timeScale) {
38
+ this._timeScale = d3
39
+ .scaleTime()
40
+ .domain([d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1)])
41
+ .range([0, this.getChartWidth()])
42
+ }
43
+ return this._timeScale
44
+ }
45
+
46
+ updateRange(fromDate: string, toDate: string) {
47
+ this.fromTime = new Date(fromDate)
48
+ this.toTime = new Date(toDate)
49
+ this.totalDays = d3.timeDay.count(this.fromTime, d3.timeDay.offset(this.toTime, 1))
50
+ this._timeScale = undefined
51
+ this.host.requestUpdate()
52
+ }
53
+
54
+ setTasks(tasks: Task[]) {
55
+ this.tasks = tasks
56
+ this.host.requestUpdate()
57
+ }
58
+
59
+ updateTimeScale() {
60
+ this.timeScale.range([0, this.getChartWidth()])
61
+ }
62
+
63
+ getChartWidth() {
64
+ return (this.totalDays * this.baseWidth) / this.getScale()
65
+ }
66
+
67
+ getScale() {
68
+ if (this.level == 'quarter-week') {
69
+ return 7 / 2
70
+ } else if (this.level == 'year-month') {
71
+ return 30 / 2
72
+ } else if (this.level == 'year-quarter') {
73
+ return 90 / 2
74
+ }
75
+ return 1
76
+ }
77
+
78
+ drawAxes(svgHeader: SVGElement) {
79
+ const header = d3.select(svgHeader)
80
+
81
+ header.selectAll('*').remove()
82
+ header.attr('width', this.getChartWidth())
83
+
84
+ const addGridLines = (ticks: Date[], yStart: number, yEnd: number) => {
85
+ ticks.forEach(tick => {
86
+ const startX = this.timeScale(tick)
87
+ header
88
+ .append('line')
89
+ .attr('x1', startX)
90
+ .attr('x2', startX)
91
+ .attr('y1', yStart)
92
+ .attr('y2', yEnd)
93
+ .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-on-secondary-container))')
94
+ .attr('stroke-width', 1)
95
+ })
96
+ }
97
+
98
+ const centerLabel = (
99
+ selection: d3.Selection<SVGGElement, unknown, null, any>,
100
+ tickInterval: d3.CountableTimeInterval
101
+ ) => {
102
+ selection.selectAll('text').attr('transform', (d: any) => {
103
+ const tickIntervalWidth = this.timeScale(tickInterval.offset(d, 1)) - this.timeScale(d)
104
+ return `translate(${tickIntervalWidth / 2}, -2)`
105
+ })
106
+ }
107
+
108
+ if (this.level === 'week-day') {
109
+ const weekAxis = d3
110
+ .axisTop(this.timeScale)
111
+ .ticks(d3.timeWeek.every(1))
112
+ .tickFormat(d3.timeFormat('%B %d') as any)
113
+ .tickSize(0)
114
+ const weekTicks = d3.timeWeek
115
+ .every(1)!
116
+ .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
117
+ addGridLines(weekTicks, 0, 20)
118
+
119
+ const weekAxisG = header
120
+ .append('g')
121
+ .classed('week-axis no-select md-typescale-label-small', true)
122
+ .attr('transform', `translate(0, 20)`)
123
+ .call(weekAxis)
124
+
125
+ centerLabel(weekAxisG, d3.timeWeek)
126
+
127
+ const dayAxis = d3
128
+ .axisTop(this.timeScale)
129
+ .ticks(d3.timeDay.every(1))
130
+ .tickFormat(d3.timeFormat('%d') as any)
131
+ .tickSize(0)
132
+
133
+ const dayTicks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
134
+ addGridLines(dayTicks, 20, 40)
135
+
136
+ const dayAxisG = header
137
+ .append('g')
138
+ .classed('day-axis no-select md-typescale-label-small', true)
139
+ .attr('transform', `translate(0, 40)`)
140
+ .call(dayAxis)
141
+
142
+ centerLabel(dayAxisG, d3.timeDay)
143
+ } else if (this.level === 'quarter-week') {
144
+ const quarterAxis = d3
145
+ .axisTop(this.timeScale)
146
+ .ticks(d3.timeMonth.every(3))
147
+ .tickFormat(d3.timeFormat('%Y %b') as any)
148
+ .tickSize(0)
149
+
150
+ const quarterTicks = d3.timeMonth
151
+ .every(3)!
152
+ .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
153
+ addGridLines(quarterTicks, 0, 20)
154
+
155
+ const quarterAxisG = header
156
+ .append('g')
157
+ .classed('quarter-axis no-select md-typescale-label-small', true)
158
+ .attr('transform', `translate(0, 20)`)
159
+ .call(quarterAxis)
160
+
161
+ centerLabel(quarterAxisG, d3.timeMonth.every(3) as any)
162
+
163
+ const weekAxis = d3
164
+ .axisTop(this.timeScale)
165
+ .ticks(d3.timeWeek.every(1))
166
+ .tickFormat(d3.timeFormat('%b %d') as any)
167
+ .tickSize(0)
168
+ const weekTicks = d3.timeWeek
169
+ .every(1)!
170
+ .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
171
+ addGridLines(weekTicks, 20, 40)
172
+
173
+ const weekAxisG = header
174
+ .append('g')
175
+ .classed('week-axis no-select md-typescale-label-small', true)
176
+ .attr('transform', `translate(0, 40)`)
177
+ .call(weekAxis)
178
+
179
+ centerLabel(weekAxisG, d3.timeWeek)
180
+ } else if (this.level === 'year-month') {
181
+ const yearAxis = d3
182
+ .axisTop(this.timeScale)
183
+ .ticks(d3.timeYear.every(1))
184
+ .tickFormat(d3.timeFormat('%Y') as any)
185
+ .tickSize(0)
186
+
187
+ const yearTicks = d3.timeYear
188
+ .every(1)!
189
+ .range(d3.timeYear.floor(this.fromTime), d3.timeYear.offset(this.toTime, 1))
190
+ addGridLines(yearTicks, 0, 20)
191
+
192
+ const yearAxisG = header
193
+ .append('g')
194
+ .classed('year-axis no-select md-typescale-label-small', true)
195
+ .attr('transform', `translate(0, 20)`)
196
+ .call(yearAxis)
197
+
198
+ centerLabel(yearAxisG, d3.timeYear.every(1)! as any)
199
+
200
+ const monthAxis = d3
201
+ .axisTop(this.timeScale)
202
+ .ticks(d3.timeMonth.every(1))
203
+ .tickFormat(d3.timeFormat('%b') as any)
204
+ .tickSize(0)
205
+
206
+ const monthTicks = d3.timeMonth
207
+ .every(1)!
208
+ .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
209
+ addGridLines(monthTicks, 20, 40)
210
+
211
+ const monthAxisG = header
212
+ .append('g')
213
+ .classed('month-axis no-select md-typescale-label-small', true)
214
+ .attr('transform', `translate(0, 40)`)
215
+ .call(monthAxis)
216
+
217
+ centerLabel(monthAxisG, d3.timeMonth.every(1) as any)
218
+ } else if (this.level === 'year-quarter') {
219
+ const yearAxis = d3
220
+ .axisTop(this.timeScale)
221
+ .ticks(d3.timeYear.every(1))
222
+ .tickFormat(d3.timeFormat('%Y') as any)
223
+ .tickSize(0)
224
+
225
+ const yearTicks = d3.timeYear
226
+ .every(1)!
227
+ .range(d3.timeYear.floor(this.fromTime), d3.timeYear.offset(this.toTime, 1))
228
+ addGridLines(yearTicks, 0, 20)
229
+
230
+ const yearAxisG = header
231
+ .append('g')
232
+ .classed('year-axis no-select md-typescale-label-small', true)
233
+ .attr('transform', `translate(0, 20)`)
234
+ .call(yearAxis)
235
+
236
+ centerLabel(yearAxisG, d3.timeYear.every(1)! as any)
237
+
238
+ const quarterAxis = d3
239
+ .axisTop(this.timeScale)
240
+ .ticks(d3.timeMonth.every(3))
241
+ .tickFormat(d3.timeFormat('%Y %b') as any)
242
+ .tickSize(0)
243
+
244
+ const quarterTicks = d3.timeMonth
245
+ .every(3)!
246
+ .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
247
+ addGridLines(quarterTicks, 20, 40)
248
+
249
+ const quarterAxisG = header
250
+ .append('g')
251
+ .classed('quarter-axis no-select md-typescale-label-small', true)
252
+ .attr('transform', `translate(0, 40)`)
253
+ .call(quarterAxis)
254
+
255
+ centerLabel(quarterAxisG, d3.timeMonth.every(3) as any)
256
+ }
257
+ }
258
+
259
+ drawBody(svgBody: SVGElement, tooltip: OxTaskTooltip) {
260
+ const body = d3.select(svgBody)
261
+
262
+ body.selectAll('*').remove()
263
+ body.attr('width', this.getChartWidth())
264
+
265
+ if (this.extendGridLines) {
266
+ const addGridLines = (ticks: Date[]) => {
267
+ ticks.forEach(tick => {
268
+ const startX = this.timeScale(tick)
269
+ body
270
+ .append('line')
271
+ .attr('x1', startX)
272
+ .attr('x2', startX)
273
+ .attr('y1', 0)
274
+ .attr('y2', Number(svgBody.getBoundingClientRect().height) || 0)
275
+ .attr('stroke', 'var(--ox-gantt-color-gridlines, var(--md-sys-color-outline))')
276
+ .attr('stroke-width', 1)
277
+ })
278
+ }
279
+
280
+ if (this.level === 'week-day') {
281
+ const dayTicks = d3.timeDay.every(1)!.range(d3.timeDay.floor(this.fromTime), d3.timeDay.offset(this.toTime, 1))
282
+ addGridLines(dayTicks)
283
+ } else if (this.level === 'quarter-week') {
284
+ const weekTicks = d3.timeWeek
285
+ .every(1)!
286
+ .range(d3.timeWeek.floor(this.fromTime), d3.timeWeek.offset(this.toTime, 1))
287
+ addGridLines(weekTicks)
288
+ } else if (this.level === 'year-month') {
289
+ const monthTicks = d3.timeMonth
290
+ .every(1)!
291
+ .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 1))
292
+ addGridLines(monthTicks)
293
+ } else if (this.level === 'year-quarter') {
294
+ const quarterTicks = d3.timeMonth
295
+ .every(3)!
296
+ .range(d3.timeMonth.floor(this.fromTime), d3.timeMonth.offset(this.toTime, 3))
297
+ addGridLines(quarterTicks)
298
+ }
299
+ }
300
+
301
+ const taskPositions: { [key: string]: { x: number; y: number; width: number } } = {}
302
+
303
+ this.tasks.forEach((task, index) => {
304
+ const startDate = new Date(task.startDate)
305
+ const endDate = new Date(task.endDate)
306
+
307
+ const startX = this.timeScale(d3.timeDay.floor(d3.min([startDate, endDate])!))
308
+ const endX = this.timeScale(d3.timeDay.offset(d3.timeDay.ceil(d3.max([startDate, endDate])!), -1))
309
+
310
+ const taskWidth = endX - startX + this.baseWidth / this.getScale()
311
+ const taskY = index * 30 + 7.5
312
+
313
+ const showTooltip = (event: MouseEvent) => {
314
+ tooltip.title = task.title
315
+ tooltip.startDate = startDate.toISOString().split('T')[0]
316
+ tooltip.endDate = endDate.toISOString().split('T')[0]
317
+ tooltip.progress = task.progress ?? 0
318
+ tooltip.type = task.type
319
+
320
+ tooltip.show(event.clientX, event.clientY)
321
+ }
322
+
323
+ const hideTooltip = () => {
324
+ tooltip.hide()
325
+ }
326
+
327
+ if (this.selectedTaskId === task.id) {
328
+ body
329
+ .append('rect')
330
+ .attr('x', 0)
331
+ .attr('y', taskY - 7.5)
332
+ .attr('width', this.getChartWidth())
333
+ .attr('height', 30)
334
+ .attr('fill', 'var(--ox-gantt-selected-row-background-color, var(--md-sys-color-surface-variant))')
335
+ }
336
+
337
+ const taskBar = body
338
+ .append('rect')
339
+ .attr('x', startX)
340
+ .attr('y', taskY)
341
+ .attr('width', taskWidth)
342
+ .attr('height', 15)
343
+ .attr('fill', this.getTaskColor(task.tags || []))
344
+ .attr('stroke', 'black')
345
+ .attr('stroke-width', 1)
346
+
347
+ taskBar
348
+ .on('click', () => {
349
+ this.selectedTaskId = task.id
350
+ const customEvent = new CustomEvent('task-clicked', {
351
+ detail: { taskId: task.id },
352
+ bubbles: true,
353
+ composed: true
354
+ })
355
+ this.host.dispatchEvent(customEvent)
356
+ this.host.requestUpdate()
357
+ })
358
+ .on('mouseover', showTooltip)
359
+ .on('mouseout', hideTooltip)
360
+
361
+ if (task.progress !== undefined) {
362
+ const progressWidth = taskWidth * (task.progress / 100)
363
+ const progressBar = body
364
+ .append('rect')
365
+ .attr('x', startX)
366
+ .attr('y', taskY + 7.5 - 3.75)
367
+ .attr('width', progressWidth)
368
+ .attr('height', 7.5)
369
+ .attr('fill', 'lightgreen')
370
+
371
+ progressBar
372
+ .on('click', () => {
373
+ this.selectedTaskId = task.id
374
+ const customEvent = new CustomEvent('task-clicked', {
375
+ detail: { taskId: task.id },
376
+ bubbles: true,
377
+ composed: true
378
+ })
379
+ this.host.dispatchEvent(customEvent)
380
+ this.host.requestUpdate()
381
+ })
382
+ .on('mouseover', showTooltip)
383
+ .on('mouseout', hideTooltip)
384
+ }
385
+
386
+ taskPositions[task.id] = { x: endX + this.baseWidth / this.getScale(), y: taskY + 7.5, width: taskWidth }
387
+ })
388
+
389
+ this.tasks.forEach(task => {
390
+ if (task.dependsOn && taskPositions[task.dependsOn]) {
391
+ const prevTask = taskPositions[task.dependsOn]
392
+ const currTask = taskPositions[task.id]
393
+
394
+ body
395
+ .append('path')
396
+ .attr(
397
+ 'd',
398
+ `
399
+ M${prevTask.x},${prevTask.y}
400
+ L${prevTask.x + 10},${prevTask.y}
401
+ ${
402
+ prevTask.x + 10 < currTask.x - 10 - currTask.width
403
+ ? `
404
+ L${(prevTask.x + currTask.x - currTask.width) / 2},${prevTask.y}
405
+ L${(prevTask.x + currTask.x - currTask.width) / 2},${currTask.y}
406
+ `
407
+ : `
408
+ L${prevTask.x + 10},${(prevTask.y + currTask.y) / 2}
409
+ L${currTask.x - 10 - currTask.width},${(prevTask.y + currTask.y) / 2}
410
+ `
411
+ }
412
+ L${currTask.x - 10 - currTask.width},${currTask.y}
413
+ L${currTask.x - currTask.width},${currTask.y}
414
+ `
415
+ )
416
+ .attr('stroke', 'black')
417
+ .attr('fill', 'none')
418
+ .attr('stroke-width', 1)
419
+ }
420
+ })
421
+ }
422
+
423
+ getTaskColor(tags: TaskTag[]): string {
424
+ if (tags.includes('critical')) {
425
+ return 'red'
426
+ } else if (tags.includes('milestone')) {
427
+ return 'blue'
428
+ } else if (tags.includes('done')) {
429
+ return 'green'
430
+ } else {
431
+ return 'grey'
432
+ }
433
+ }
434
+
435
+ handleWheelForBody(event: WheelEvent, container: HTMLDivElement) {
436
+ event.preventDefault()
437
+ event.stopPropagation()
438
+
439
+ if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
440
+ container.scrollLeft += event.shiftKey ? event.deltaX : event.deltaY
441
+ } else {
442
+ container.scrollTop += event.deltaY
443
+ }
444
+
445
+ const customEvent = new CustomEvent('scroll', {
446
+ detail: { offset: event.shiftKey ? container.scrollLeft : container.scrollTop },
447
+ bubbles: true,
448
+ composed: true
449
+ })
450
+
451
+ this.host.dispatchEvent(customEvent)
452
+ }
453
+
454
+ handleTouchStartForBody(event: TouchEvent, container: HTMLDivElement) {
455
+ event.stopPropagation()
456
+
457
+ this.touchStartY = event.touches[0].clientY
458
+ this.scrollStartY = container.scrollTop
459
+ }
460
+
461
+ handleTouchMoveForBody(event: TouchEvent, container: HTMLDivElement) {
462
+ const touchCurrentY = event.touches[0].clientY
463
+ const deltaY = this.touchStartY! - touchCurrentY
464
+ container.scrollTop = this.scrollStartY! + deltaY
465
+
466
+ const customEvent = new CustomEvent('scroll', {
467
+ detail: { offset: container.scrollTop },
468
+ bubbles: true,
469
+ composed: true
470
+ })
471
+ this.host.dispatchEvent(customEvent)
472
+ }
473
+ }
package/src/locales.ts ADDED
@@ -0,0 +1,78 @@
1
+ export const locales = {
2
+ 'en-US': {
3
+ dateTime: '%x, %X',
4
+ date: '%-m/%-d/%Y',
5
+ time: '%-I:%M:%S %p',
6
+ periods: ['AM', 'PM'],
7
+ days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
8
+ shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
9
+ months: [
10
+ 'January',
11
+ 'February',
12
+ 'March',
13
+ 'April',
14
+ 'May',
15
+ 'June',
16
+ 'July',
17
+ 'August',
18
+ 'September',
19
+ 'October',
20
+ 'November',
21
+ 'December'
22
+ ],
23
+ shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
24
+ },
25
+ 'ko-KR': {
26
+ dateTime: '%x, %X',
27
+ date: '%Y-%m-%d',
28
+ time: '%H:%M:%S',
29
+ periods: ['오전', '오후'],
30
+ days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'],
31
+ shortDays: ['일', '월', '화', '수', '목', '금', '토'],
32
+ months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
33
+ shortMonths: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']
34
+ },
35
+ 'ja-JP': {
36
+ dateTime: '%x, %X',
37
+ date: '%Y/%m/%d',
38
+ time: '%H:%M:%S',
39
+ periods: ['午前', '午後'],
40
+ days: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'],
41
+ shortDays: ['日', '月', '火', '水', '木', '金', '土'],
42
+ months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
43
+ shortMonths: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
44
+ },
45
+ 'zh-CN': {
46
+ dateTime: '%x, %X',
47
+ date: '%Y-%m-%d',
48
+ time: '%H:%M:%S',
49
+ periods: ['上午', '下午'],
50
+ days: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
51
+ shortDays: ['日', '一', '二', '三', '四', '五', '六'],
52
+ months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
53
+ shortMonths: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
54
+ },
55
+ 'ms-MY': {
56
+ dateTime: '%x, %X',
57
+ date: '%d/%m/%Y',
58
+ time: '%H:%M:%S',
59
+ periods: ['AM', 'PM'],
60
+ days: ['Ahad', 'Isnin', 'Selasa', 'Rabu', 'Khamis', 'Jumaat', 'Sabtu'],
61
+ shortDays: ['Ahd', 'Isn', 'Sel', 'Rab', 'Kha', 'Jum', 'Sab'],
62
+ months: [
63
+ 'Januari',
64
+ 'Februari',
65
+ 'Mac',
66
+ 'April',
67
+ 'Mei',
68
+ 'Jun',
69
+ 'Julai',
70
+ 'Ogos',
71
+ 'September',
72
+ 'Oktober',
73
+ 'November',
74
+ 'Disember'
75
+ ],
76
+ shortMonths: ['Jan', 'Feb', 'Mac', 'Apr', 'Mei', 'Jun', 'Jul', 'Ogo', 'Sep', 'Okt', 'Nov', 'Dis']
77
+ }
78
+ }