@operato/gantt 2.0.0-beta.14 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/src/gantt-chart-controller.d.ts +32 -0
  3. package/dist/src/gantt-chart-controller.js +393 -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 +10 -17
  9. package/dist/src/ox-gantt-chart.js +134 -440
  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 +13 -1
  15. package/dist/src/ox-gantt.js +144 -27
  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 +10 -3
  21. package/dist/src/types.js.map +1 -1
  22. package/dist/stories/ox-gantt.stories.js +91 -85
  23. package/dist/stories/ox-gantt.stories.js.map +1 -1
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +3 -2
  26. package/src/gantt-chart-controller.ts +470 -0
  27. package/src/locales.ts +78 -0
  28. package/src/ox-gantt-chart.ts +141 -487
  29. package/src/ox-gantt-resize-splitter.ts +18 -25
  30. package/src/ox-gantt.ts +151 -29
  31. package/src/ox-task-tooltip.ts +120 -0
  32. package/src/types.ts +11 -3
  33. package/stories/ox-gantt.stories.ts +93 -85
  34. package/translations/en.json +11 -1
  35. package/translations/ja.json +11 -1
  36. package/translations/ko.json +11 -1
  37. package/translations/ms.json +11 -1
  38. package/translations/zh.json +11 -1
  39. package/dist/src/locale.d.ts +0 -1
  40. package/dist/src/locale.js +0 -49
  41. package/dist/src/locale.js.map +0 -1
  42. package/src/locale.ts +0 -51
@@ -0,0 +1,470 @@
1
+ import { LitElement, ReactiveController, ReactiveControllerHost } from 'lit'
2
+ import * as d3 from 'd3'
3
+ import { LEVEL, 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: LEVEL
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: LEVEL) {
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-outline))')
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 startX = this.timeScale(d3.timeDay.floor(d3.min([task.startDate, task.endDate])!))
305
+ const endX = this.timeScale(d3.timeDay.offset(d3.timeDay.ceil(d3.max([task.startDate, task.endDate])!), -1))
306
+
307
+ const taskWidth = endX - startX + this.baseWidth / this.getScale()
308
+ const taskY = index * 30 + 7.5
309
+
310
+ const showTooltip = (event: MouseEvent) => {
311
+ tooltip.title = task.title
312
+ tooltip.startDate = task.startDate.toISOString().split('T')[0]
313
+ tooltip.endDate = task.endDate.toISOString().split('T')[0]
314
+ tooltip.progress = task.progress ?? 0
315
+ tooltip.type = task.type
316
+
317
+ tooltip.show(event.clientX, event.clientY)
318
+ }
319
+
320
+ const hideTooltip = () => {
321
+ tooltip.hide()
322
+ }
323
+
324
+ if (this.selectedTaskId === task.id) {
325
+ body
326
+ .append('rect')
327
+ .attr('x', 0)
328
+ .attr('y', taskY - 7.5)
329
+ .attr('width', this.getChartWidth())
330
+ .attr('height', 30)
331
+ .attr('fill', 'var(--ox-gantt-selected-row-background-color, var(--md-sys-color-surface-variant))')
332
+ }
333
+
334
+ const taskBar = body
335
+ .append('rect')
336
+ .attr('x', startX)
337
+ .attr('y', taskY)
338
+ .attr('width', taskWidth)
339
+ .attr('height', 15)
340
+ .attr('fill', this.getTaskColor(task.tags || []))
341
+ .attr('stroke', 'black')
342
+ .attr('stroke-width', 1)
343
+
344
+ taskBar
345
+ .on('click', () => {
346
+ this.selectedTaskId = task.id
347
+ const customEvent = new CustomEvent('task-clicked', {
348
+ detail: { taskId: task.id },
349
+ bubbles: true,
350
+ composed: true
351
+ })
352
+ this.host.dispatchEvent(customEvent)
353
+ this.host.requestUpdate()
354
+ })
355
+ .on('mouseover', showTooltip)
356
+ .on('mouseout', hideTooltip)
357
+
358
+ if (task.progress !== undefined) {
359
+ const progressWidth = taskWidth * (task.progress / 100)
360
+ const progressBar = body
361
+ .append('rect')
362
+ .attr('x', startX)
363
+ .attr('y', taskY + 7.5 - 3.75)
364
+ .attr('width', progressWidth)
365
+ .attr('height', 7.5)
366
+ .attr('fill', 'lightgreen')
367
+
368
+ progressBar
369
+ .on('click', () => {
370
+ this.selectedTaskId = task.id
371
+ const customEvent = new CustomEvent('task-clicked', {
372
+ detail: { taskId: task.id },
373
+ bubbles: true,
374
+ composed: true
375
+ })
376
+ this.host.dispatchEvent(customEvent)
377
+ this.host.requestUpdate()
378
+ })
379
+ .on('mouseover', showTooltip)
380
+ .on('mouseout', hideTooltip)
381
+ }
382
+
383
+ taskPositions[task.id] = { x: endX + this.baseWidth / this.getScale(), y: taskY + 7.5, width: taskWidth }
384
+ })
385
+
386
+ this.tasks.forEach(task => {
387
+ if (task.dependsOn && taskPositions[task.dependsOn]) {
388
+ const prevTask = taskPositions[task.dependsOn]
389
+ const currTask = taskPositions[task.id]
390
+
391
+ body
392
+ .append('path')
393
+ .attr(
394
+ 'd',
395
+ `
396
+ M${prevTask.x},${prevTask.y}
397
+ L${prevTask.x + 10},${prevTask.y}
398
+ ${
399
+ prevTask.x + 10 < currTask.x - 10 - currTask.width
400
+ ? `
401
+ L${(prevTask.x + currTask.x - currTask.width) / 2},${prevTask.y}
402
+ L${(prevTask.x + currTask.x - currTask.width) / 2},${currTask.y}
403
+ `
404
+ : `
405
+ L${prevTask.x + 10},${(prevTask.y + currTask.y) / 2}
406
+ L${currTask.x - 10 - currTask.width},${(prevTask.y + currTask.y) / 2}
407
+ `
408
+ }
409
+ L${currTask.x - 10 - currTask.width},${currTask.y}
410
+ L${currTask.x - currTask.width},${currTask.y}
411
+ `
412
+ )
413
+ .attr('stroke', 'black')
414
+ .attr('fill', 'none')
415
+ .attr('stroke-width', 1)
416
+ }
417
+ })
418
+ }
419
+
420
+ getTaskColor(tags: TaskTag[]): string {
421
+ if (tags.includes('critical')) {
422
+ return 'red'
423
+ } else if (tags.includes('milestone')) {
424
+ return 'blue'
425
+ } else if (tags.includes('done')) {
426
+ return 'green'
427
+ } else {
428
+ return 'grey'
429
+ }
430
+ }
431
+
432
+ handleWheelForBody(event: WheelEvent, container: HTMLDivElement) {
433
+ event.preventDefault()
434
+ event.stopPropagation()
435
+
436
+ if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
437
+ container.scrollLeft += event.shiftKey ? event.deltaX : event.deltaY
438
+ } else {
439
+ container.scrollTop += event.deltaY
440
+ }
441
+
442
+ const customEvent = new CustomEvent('scroll', {
443
+ detail: { offset: event.shiftKey ? container.scrollLeft : container.scrollTop },
444
+ bubbles: true,
445
+ composed: true
446
+ })
447
+
448
+ this.host.dispatchEvent(customEvent)
449
+ }
450
+
451
+ handleTouchStartForBody(event: TouchEvent, container: HTMLDivElement) {
452
+ event.stopPropagation()
453
+
454
+ this.touchStartY = event.touches[0].clientY
455
+ this.scrollStartY = container.scrollTop
456
+ }
457
+
458
+ handleTouchMoveForBody(event: TouchEvent, container: HTMLDivElement) {
459
+ const touchCurrentY = event.touches[0].clientY
460
+ const deltaY = this.touchStartY! - touchCurrentY
461
+ container.scrollTop = this.scrollStartY! + deltaY
462
+
463
+ const customEvent = new CustomEvent('scroll', {
464
+ detail: { offset: container.scrollTop },
465
+ bubbles: true,
466
+ composed: true
467
+ })
468
+ this.host.dispatchEvent(customEvent)
469
+ }
470
+ }
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
+ }