-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathTable.lua
More file actions
445 lines (400 loc) · 15.1 KB
/
Table.lua
File metadata and controls
445 lines (400 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
---@type string
local addonName = select(1, ...)
---@class WK_Addon
local addon = select(2, ...)
---@class WK_TableModule
local Table = {}
addon.Table = Table
Table.collection = {}
local Utils = addon.Utils
local Constants = addon.Constants
local UI = addon.UI
---@param config WK_TableConfig?
---@return Frame
function Table:CreateFrame(config)
local tableCount = Utils:TableCount(self.collection)
local tableFrame = CreateFrame("Frame", "WeeklyKnowledgeTable" .. (tableCount + 1))
---@type WK_TableConfig
local defaultTableConfig = {
header = {
enabled = true,
sticky = false,
height = 30,
},
rows = {
height = 22,
highlight = true,
striped = true
},
columns = {
width = 100,
highlight = false,
striped = false
},
cells = {
padding = Constants.TABLE_CELL_PADDING,
highlight = false
},
sorting = {
enabled = false,
defaultOrder = "desc",
defaultCompare = function(_, _)
return false
end,
},
data = {
columns = {},
rows = {},
},
}
local mergedConfig = CopyTable(defaultTableConfig)
Utils:TableMergeDeep(mergedConfig, config or {})
tableFrame.config = mergedConfig
do
local sorting = tableFrame.config.sorting
if sorting and sorting.enabled then
if type(sorting.defaultCompare) ~= "function" then
error("WeeklyKnowledge Table: sorting.enabled requires sorting.defaultCompare", 2)
end
if sorting.defaultOrder ~= "asc" and sorting.defaultOrder ~= "desc" then
error("WeeklyKnowledge Table: sorting.enabled requires sorting.defaultOrder to be \"asc\" or \"desc\"", 2)
end
end
end
tableFrame.rows = {}
tableFrame.data = tableFrame.config.data
---@type WK_TableSortState
tableFrame.sortState = {columnId = nil, direction = nil}
---@param columnId string?
---@return number|nil
function tableFrame:ColumnIndexForId(columnId)
if not columnId or not self.data or not self.data.columns then
return nil
end
for i, col in ipairs(self.data.columns) do
if col.id == columnId then
return i
end
end
return nil
end
function tableFrame:SetSortStateToDefault()
local state = self.sortState
state.columnId = nil
state.direction = nil
end
function tableFrame:ValidateSortState()
local sorting = self.config.sorting
if not sorting or not sorting.enabled then
return
end
local state = self.sortState
if not state then
return
end
if state.columnId and not self:ColumnIndexForId(state.columnId) then
self:SetSortStateToDefault()
if sorting.onStateChanged then
sorting.onStateChanged(state)
end
return
end
if state.columnId then
if state.direction ~= "asc" and state.direction ~= "desc" then
state.direction = (sorting.defaultOrder == "asc") and "asc" or "desc"
end
else
state.direction = nil
end
end
do
local sorting = tableFrame.config.sorting
local saved = sorting and sorting.savedState
if sorting and sorting.enabled and saved and type(saved.columnId) == "string" and saved.columnId ~= "" then
tableFrame.sortState.columnId = saved.columnId
if saved.direction == "asc" or saved.direction == "desc" then
tableFrame.sortState.direction = saved.direction
else
tableFrame.sortState.direction = (sorting.defaultOrder == "asc") and "asc" or "desc"
end
else
tableFrame:SetSortStateToDefault()
end
end
local function notifySortStateChanged()
local sorting = tableFrame.config.sorting
if sorting and sorting.onStateChanged then
sorting.onStateChanged(tableFrame.sortState)
end
end
tableFrame.scrollFrame = UI:CreateScrollFrame({
name = "$parentScrollFrame",
scrollSpeedVertical = tableFrame.config.rows.height * 2
})
function tableFrame:ApplySortToData()
local sorting = self.config.sorting
if not sorting or not sorting.enabled then return end
local rows = self.data.rows
if not rows or #rows <= 1 then return end
local headerEnabled = self.config.header.enabled
local dataStart = headerEnabled and 2 or 1
if not rows[dataStart] then return end
local state = self.sortState
local sortColumnIndex = self:ColumnIndexForId(state.columnId)
if state.columnId and state.direction and not sortColumnIndex then
return
end
local dataRows = {}
for i = dataStart, #rows do
dataRows[#dataRows + 1] = rows[i]
end
local columnSort = state.columnId and state.direction and sortColumnIndex
if not columnSort then
table.sort(dataRows, sorting.defaultCompare)
for i = 1, #dataRows do
rows[dataStart + i - 1] = dataRows[i]
end
return
end
local ascending = state.direction == "asc"
local colCfg = self.data.columns[sortColumnIndex]
if not colCfg or not colCfg.sorting then
error(format("WeeklyKnowledge Table: column \"%s\" must define sorting", tostring(colCfg and colCfg.id)), 2)
end
local columnSorting = colCfg.sorting
if not columnSorting.enabled then
error(format("WeeklyKnowledge Table: column \"%s\" is not sortable (sorting.enabled is false)", tostring(colCfg.id)), 2)
end
if type(columnSorting.compare) ~= "function" then
error(format("WeeklyKnowledge Table: column \"%s\" must define sorting.compare when sorting.enabled is true", tostring(colCfg.id)), 2)
end
table.sort(dataRows, function(a, b)
if ascending then
return columnSorting.compare(a, b)
end
return columnSorting.compare(b, a)
end)
for i = 1, #dataRows do
rows[dataStart + i - 1] = dataRows[i]
end
end
---@param columnId string Stable column id
---@param button string? "LeftButton"|"RightButton"|… (from Button OnClick)
function tableFrame:OnHeaderColumnClick(columnId, button)
if type(columnId) ~= "string" or columnId == "" then return end
local state = self.sortState
if not state then
state = {columnId = nil, direction = nil}
self.sortState = state
end
if button == "RightButton" then
self:SetSortStateToDefault()
self:ApplySortToData()
self:RenderTable()
notifySortStateChanged()
return
end
local sortingCfg = self.config.sorting
if state.columnId == columnId then
state.direction = (state.direction == "asc") and "desc" or "asc"
else
state.columnId = columnId
state.direction = (sortingCfg and sortingCfg.defaultOrder == "asc") and "asc" or "desc"
end
self:ApplySortToData()
self:RenderTable()
notifySortStateChanged()
end
function tableFrame:SetData(data)
self.data = data
if data and data.columns then
for i, col in ipairs(data.columns) do
if not col.sorting then
error(format('WeeklyKnowledge Table: column #%d ("%s") must define sorting', i, tostring(col.id)), 2)
end
end
end
self:ValidateSortState()
self:ApplySortToData()
self:RenderTable()
end
function tableFrame:SetRowHeight(height)
self.config.rows.height = height
self:RenderTable()
end
function tableFrame:RenderTable()
local offsetY = 0
local offsetX = 0
Utils:TableForEach(tableFrame.rows, function(rowFrame) rowFrame:Hide() end)
Utils:TableForEach(tableFrame.data.rows, function(row, rowIndex)
local rowFrame = tableFrame.rows[rowIndex]
local rowHeight = tableFrame.config.rows.height
local isStickyRow = false
local isHeaderRow = (rowIndex == 1 and tableFrame.config.header.enabled)
if not rowFrame then
rowFrame = CreateFrame("Button", "$parentRow" .. rowIndex, tableFrame)
rowFrame.columns = {}
tableFrame.rows[rowIndex] = rowFrame
end
if rowIndex == 1 then
if tableFrame.config.header.enabled then
rowHeight = tableFrame.config.header.height
end
if tableFrame.config.header.sticky then
isStickyRow = true
end
end
-- Sticky header
if isStickyRow then
rowFrame:SetParent(tableFrame)
rowFrame:SetPoint("TOPLEFT", tableFrame, "TOPLEFT", 0, 0)
rowFrame:SetPoint("TOPRIGHT", tableFrame, "TOPRIGHT", 0, 0)
if not row.backgroundColor then
Utils:SetBackgroundColor(rowFrame, 0, 0, 0, 0.3)
end
else
rowFrame:SetParent(tableFrame.scrollFrame.content)
rowFrame:SetPoint("TOPLEFT", tableFrame.scrollFrame.content, "TOPLEFT", 0, -offsetY)
rowFrame:SetPoint("TOPRIGHT", tableFrame.scrollFrame.content, "TOPRIGHT", 0, -offsetY)
if tableFrame.config.rows.striped and rowIndex % 2 == 1 then
Utils:SetBackgroundColor(rowFrame, 1, 1, 1, .02)
end
end
if row.backgroundColor then
Utils:SetBackgroundColor(rowFrame, row.backgroundColor.r, row.backgroundColor.g, row.backgroundColor.b, row.backgroundColor.a)
end
rowFrame.data = row
rowFrame:SetHeight(rowHeight)
rowFrame:SetScript("OnEnter", function() rowFrame:onEnterHandler(rowFrame) end)
rowFrame:SetScript("OnLeave", function() rowFrame:onLeaveHandler(rowFrame) end)
rowFrame:SetScript("OnClick", function(_, button, ...)
rowFrame:onClickHandler(rowFrame, button)
end)
rowFrame:Show()
function rowFrame:onEnterHandler(f)
if rowIndex > 1 or not tableFrame.config.header.enabled then
Utils:SetHighlightColor(rowFrame, 1, 1, 1, .03)
end
if row.onEnter then
row.onEnter(f)
end
end
function rowFrame:onLeaveHandler(f)
if rowIndex > 1 or not tableFrame.config.header.enabled then
Utils:SetHighlightColor(rowFrame, 1, 1, 1, 0)
end
if row.onLeave then
row.onLeave(f)
end
end
function rowFrame:onClickHandler(f, button)
if row.onClick then
row.onClick(f, button)
end
end
offsetX = 0
Utils:TableForEach(rowFrame.columns, function(columnFrame) columnFrame:Hide() end)
Utils:TableForEach(row.cells, function(column, columnIndex)
local columnFrame = rowFrame.columns[columnIndex]
local columnConfig = tableFrame.data.columns[columnIndex]
local columnWidth = columnConfig and columnConfig.width or tableFrame.config.columns.width
local columnTextAlign = columnConfig and columnConfig.align or "LEFT"
if not columnFrame then
columnFrame = CreateFrame("Button", "$parentCol" .. columnIndex, rowFrame)
columnFrame.text = columnFrame:CreateFontString("$parentText", "OVERLAY")
columnFrame.text:SetFontObject("GameFontHighlightSmall")
rowFrame.columns[columnIndex] = columnFrame
end
columnFrame.data = column
columnFrame:SetPoint("TOPLEFT", rowFrame, "TOPLEFT", offsetX, 0)
columnFrame:SetPoint("BOTTOMLEFT", rowFrame, "BOTTOMLEFT", offsetX, 0)
columnFrame:SetWidth(columnWidth)
columnFrame:SetScript("OnEnter", function() columnFrame:onEnterHandler(columnFrame) end)
columnFrame:SetScript("OnLeave", function() columnFrame:onLeaveHandler(columnFrame) end)
columnFrame:SetScript("OnClick", function(_, button, ...)
columnFrame:onClickHandler(columnFrame, button)
end)
if isHeaderRow and tableFrame.config.sorting and tableFrame.config.sorting.enabled then
columnFrame:RegisterForClicks("LeftButtonUp", "RightButtonUp")
end
columnFrame.text:SetWordWrap(false)
columnFrame.text:SetJustifyH(columnTextAlign)
columnFrame.text:SetPoint("TOPLEFT", columnFrame, "TOPLEFT", tableFrame.config.cells.padding, -tableFrame.config.cells.padding)
columnFrame.text:SetPoint("BOTTOMRIGHT", columnFrame, "BOTTOMRIGHT", -tableFrame.config.cells.padding, tableFrame.config.cells.padding)
columnFrame.text:SetText(column.text)
columnFrame:Show()
if column.backgroundColor then
Utils:SetBackgroundColor(columnFrame, column.backgroundColor.r, column.backgroundColor.g, column.backgroundColor.b, column.backgroundColor.a)
end
if isHeaderRow and tableFrame.config.sorting and tableFrame.config.sorting.enabled then
local colCfg = tableFrame.data.columns[columnIndex]
local state = tableFrame.sortState
local show = colCfg and colCfg.sorting.enabled and state
and state.columnId == colCfg.id and state.direction ~= nil
if show then
Utils:SetHighlightColor(columnFrame, 1, 1, 1, 0.03)
else
Utils:SetHighlightColor(columnFrame, 1, 1, 1, 0)
end
end
function columnFrame:onEnterHandler(f)
rowFrame:onEnterHandler(f)
if column.onEnter then
column.onEnter(f)
end
if isHeaderRow and tableFrame.config.sorting and tableFrame.config.sorting.enabled then
local colCfg = tableFrame.data.columns[columnIndex]
if colCfg and colCfg.sorting.enabled then
if not column.onEnter then
GameTooltip:SetOwner(f, "ANCHOR_RIGHT")
GameTooltip:SetText(colCfg.headerText or "", 1, 1, 1)
else
GameTooltip:AddLine(" ")
end
GameTooltip:AddLine("<Click to Sort>", GREEN_FONT_COLOR.r, GREEN_FONT_COLOR.g, GREEN_FONT_COLOR.b)
GameTooltip:AddLine("<Right Click to Reset>", GREEN_FONT_COLOR.r, GREEN_FONT_COLOR.g, GREEN_FONT_COLOR.b)
GameTooltip:Show()
end
end
end
function columnFrame:onLeaveHandler(f)
rowFrame:onLeaveHandler(f)
if column.onLeave then
column.onLeave(f)
end
if isHeaderRow and tableFrame.config.sorting and tableFrame.config.sorting.enabled then
local colCfg = tableFrame.data.columns[columnIndex]
if colCfg and colCfg.sorting.enabled and not column.onLeave then
GameTooltip:Hide()
end
end
end
function columnFrame:onClickHandler(f, button)
if isHeaderRow and tableFrame.config.sorting and tableFrame.config.sorting.enabled then
local colCfg = tableFrame.data.columns[columnIndex]
if colCfg and colCfg.sorting.enabled then
tableFrame:OnHeaderColumnClick(colCfg.id, button)
return
end
end
rowFrame:onClickHandler(f, button)
if column.onClick then
column.onClick(f, button)
end
end
offsetX = offsetX + columnWidth
end)
if not isStickyRow then
offsetY = offsetY + rowHeight
end
end)
tableFrame.scrollFrame:SetParent(tableFrame)
tableFrame.scrollFrame:SetPoint("TOPLEFT", tableFrame, "TOPLEFT", 0, tableFrame.config.header.sticky and -tableFrame.config.header.height or 0)
tableFrame.scrollFrame:SetPoint("BOTTOMRIGHT", tableFrame, "BOTTOMRIGHT")
tableFrame.scrollFrame.content:SetSize(offsetX, offsetY)
end
tableFrame.scrollFrame:HookScript("OnSizeChanged", function() tableFrame:RenderTable() end)
tableFrame:RenderTable()
table.insert(self.collection, tableFrame)
return tableFrame
end