data-table.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. package vuetifyx
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. v "github.com/qor5/ui/vuetify"
  7. "github.com/qor5/web"
  8. "github.com/rs/xid"
  9. "github.com/sunfmin/reflectutils"
  10. h "github.com/theplant/htmlgo"
  11. "github.com/thoas/go-funk"
  12. )
  13. type CellComponentFunc func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent
  14. type CellWrapperFunc func(cell h.MutableAttrHTMLComponent, id string, obj interface{}, dataTableID string) h.HTMLComponent
  15. type HeadCellWrapperFunc func(cell h.MutableAttrHTMLComponent, field string, title string) h.HTMLComponent
  16. type RowWrapperFunc func(row h.MutableAttrHTMLComponent, id string, obj interface{}, dataTableID string) h.HTMLComponent
  17. type RowMenuItemFunc func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent
  18. type RowComponentFunc func(obj interface{}, ctx *web.EventContext) h.HTMLComponent
  19. type OnSelectFunc func(id string, ctx *web.EventContext) string
  20. type OnSelectAllFunc func(idsOfPage []string, ctx *web.EventContext) string
  21. type OnClearSelectionFunc func(ctx *web.EventContext) string
  22. type DataTableBuilder struct {
  23. data interface{}
  24. selectable bool
  25. withoutHeaders bool
  26. selectionParamName string
  27. cellWrapper CellWrapperFunc
  28. headCellWrapper HeadCellWrapperFunc
  29. rowWrapper RowWrapperFunc
  30. rowMenuItemFuncs []RowMenuItemFunc
  31. rowExpandFunc RowComponentFunc
  32. columns []*DataTableColumnBuilder
  33. loadMoreCount int
  34. loadMoreLabel string
  35. loadMoreURL string
  36. // e.g. {count} records are selected.
  37. selectedCountLabel string
  38. clearSelectionLabel string
  39. onClearSelectionFunc OnClearSelectionFunc
  40. tfootChildren []h.HTMLComponent
  41. selectableColumnsBtn h.HTMLComponent
  42. onSelectFunc OnSelectFunc
  43. onSelectAllFunc OnSelectAllFunc
  44. }
  45. func DataTable(data interface{}) (r *DataTableBuilder) {
  46. r = &DataTableBuilder{
  47. data: data,
  48. selectionParamName: "selected_ids",
  49. selectedCountLabel: "{count} records are selected.",
  50. clearSelectionLabel: "clear selection",
  51. }
  52. return
  53. }
  54. func (b *DataTableBuilder) LoadMoreAt(count int, label string) (r *DataTableBuilder) {
  55. b.loadMoreCount = count
  56. b.loadMoreLabel = label
  57. return b
  58. }
  59. func (b *DataTableBuilder) LoadMoreURL(url string) (r *DataTableBuilder) {
  60. b.loadMoreURL = url
  61. return b
  62. }
  63. func (b *DataTableBuilder) Tfoot(children ...h.HTMLComponent) (r *DataTableBuilder) {
  64. b.tfootChildren = children
  65. return b
  66. }
  67. func (b *DataTableBuilder) Selectable(v bool) (r *DataTableBuilder) {
  68. b.selectable = v
  69. return b
  70. }
  71. func (b *DataTableBuilder) Data(v interface{}) (r *DataTableBuilder) {
  72. b.data = v
  73. return b
  74. }
  75. func (b *DataTableBuilder) SelectionParamName(v string) (r *DataTableBuilder) {
  76. b.selectionParamName = v
  77. return b
  78. }
  79. func (b *DataTableBuilder) WithoutHeader(v bool) (r *DataTableBuilder) {
  80. b.withoutHeaders = v
  81. return b
  82. }
  83. func (b *DataTableBuilder) CellWrapperFunc(v CellWrapperFunc) (r *DataTableBuilder) {
  84. b.cellWrapper = v
  85. return b
  86. }
  87. func (b *DataTableBuilder) HeadCellWrapperFunc(v HeadCellWrapperFunc) (r *DataTableBuilder) {
  88. b.headCellWrapper = v
  89. return b
  90. }
  91. func (b *DataTableBuilder) RowWrapperFunc(v RowWrapperFunc) (r *DataTableBuilder) {
  92. b.rowWrapper = v
  93. return b
  94. }
  95. func (b *DataTableBuilder) RowMenuItemFuncs(vs ...RowMenuItemFunc) (r *DataTableBuilder) {
  96. b.rowMenuItemFuncs = vs
  97. return b
  98. }
  99. func (b *DataTableBuilder) RowMenuItemFunc(v RowMenuItemFunc) (r *DataTableBuilder) {
  100. b.rowMenuItemFuncs = append(b.rowMenuItemFuncs, v)
  101. return b
  102. }
  103. func (b *DataTableBuilder) RowExpandFunc(v RowComponentFunc) (r *DataTableBuilder) {
  104. b.rowExpandFunc = v
  105. return b
  106. }
  107. func (b *DataTableBuilder) SelectedCountLabel(v string) (r *DataTableBuilder) {
  108. b.selectedCountLabel = v
  109. return b
  110. }
  111. func (b *DataTableBuilder) ClearSelectionLabel(v string) (r *DataTableBuilder) {
  112. b.clearSelectionLabel = v
  113. return b
  114. }
  115. func (b *DataTableBuilder) OnClearSelectionFunc(v OnClearSelectionFunc) (r *DataTableBuilder) {
  116. b.onClearSelectionFunc = v
  117. return b
  118. }
  119. func (b *DataTableBuilder) SelectableColumnsBtn(v h.HTMLComponent) (r *DataTableBuilder) {
  120. b.selectableColumnsBtn = v
  121. return b
  122. }
  123. func (b *DataTableBuilder) OnSelectAllFunc(v OnSelectAllFunc) (r *DataTableBuilder) {
  124. b.onSelectAllFunc = v
  125. return b
  126. }
  127. func (b *DataTableBuilder) OnSelectFunc(v OnSelectFunc) (r *DataTableBuilder) {
  128. b.onSelectFunc = v
  129. return b
  130. }
  131. type primarySlugger interface {
  132. PrimarySlug() string
  133. }
  134. func (b *DataTableBuilder) MarshalHTML(c context.Context) (r []byte, err error) {
  135. ctx := web.MustGetEventContext(c)
  136. selected := getSelectedIds(ctx, b.selectionParamName)
  137. dataTableId := xid.New().String()
  138. loadMoreVarName := fmt.Sprintf("loadmore_%s", dataTableId)
  139. expandVarName := fmt.Sprintf("expand_%s", dataTableId)
  140. selectedCountVarName := fmt.Sprintf("selected_count_%s", dataTableId)
  141. initContextVarsMap := map[string]interface{}{
  142. selectedCountVarName: len(selected),
  143. }
  144. // map[obj_id]{rowMenus}
  145. objRowMenusMap := make(map[string][]h.HTMLComponent)
  146. funk.ForEach(b.data, func(obj interface{}) {
  147. id := ObjectID(obj)
  148. var opMenuItems []h.HTMLComponent
  149. for _, f := range b.rowMenuItemFuncs {
  150. item := f(obj, id, ctx)
  151. if item == nil {
  152. continue
  153. }
  154. opMenuItems = append(opMenuItems, item)
  155. }
  156. if len(opMenuItems) > 0 {
  157. objRowMenusMap[id] = opMenuItems
  158. }
  159. })
  160. hasRowMenuCol := len(objRowMenusMap) > 0 || b.selectableColumnsBtn != nil
  161. var rows []h.HTMLComponent
  162. var idsOfPage []string
  163. inPlaceLoadMore := b.loadMoreCount > 0 && b.loadMoreURL == ""
  164. hasExpand := b.rowExpandFunc != nil
  165. i := 0
  166. tdCount := 0
  167. haveMoreRecord := false
  168. funk.ForEach(b.data, func(obj interface{}) {
  169. id := ObjectID(obj)
  170. idsOfPage = append(idsOfPage, id)
  171. inputValue := ""
  172. if funk.ContainsString(selected, id) {
  173. inputValue = id
  174. }
  175. var tds []h.HTMLComponent
  176. if hasExpand {
  177. initContextVarsMap[fmt.Sprintf("%s_%d", expandVarName, i)] = false
  178. tds = append(tds, h.Td(
  179. v.VIcon("$vuetify.icons.expand").
  180. Attr(":class", fmt.Sprintf("{\"v-data-table__expand-icon--active\": vars.%s_%d, \"v-data-table__expand-icon\": true}", expandVarName, i)).
  181. On("click", fmt.Sprintf("vars.%s_%d = !vars.%s_%d", expandVarName, i, expandVarName, i)),
  182. ).Class("pr-0").Style("width: 40px;"))
  183. }
  184. if b.selectable {
  185. onChange := web.Plaid().
  186. PushState(true).
  187. MergeQuery(true).
  188. Query(b.selectionParamName,
  189. web.Var(fmt.Sprintf(`{value: %s, add: $event, remove: !$event}`, h.JSONString(id))),
  190. ).RunPushState()
  191. if b.onSelectFunc != nil {
  192. onChange = b.onSelectFunc(id, ctx)
  193. }
  194. tds = append(tds, h.Td(
  195. v.VCheckbox().
  196. Class("mt-0").
  197. InputValue(inputValue).
  198. TrueValue(id).
  199. FalseValue("").
  200. HideDetails(true).
  201. Attr("@change", onChange+fmt.Sprintf(";vars.%s+=($event?1:-1)", selectedCountVarName)),
  202. ).Class("pr-0"))
  203. }
  204. for _, f := range b.columns {
  205. tds = append(tds, f.cellComponentFunc(obj, f.name, ctx))
  206. }
  207. var bindTds []h.HTMLComponent
  208. for _, td := range tds {
  209. std, ok := td.(h.MutableAttrHTMLComponent)
  210. if !ok {
  211. bindTds = append(bindTds, td)
  212. continue
  213. }
  214. var tdWrapped h.HTMLComponent = std
  215. if b.cellWrapper != nil {
  216. tdWrapped = b.cellWrapper(std, id, obj, dataTableId)
  217. }
  218. bindTds = append(bindTds, tdWrapped)
  219. }
  220. if hasRowMenuCol {
  221. var td h.HTMLComponent
  222. rowMenus, ok := objRowMenusMap[id]
  223. if ok {
  224. td = h.Td(
  225. v.VMenu(
  226. web.Slot(
  227. v.VBtn("").Children(
  228. v.VIcon("more_horiz"),
  229. ).Attr("v-on", "on").Text(true).Fab(true).Small(true),
  230. ).Name("activator").Scope("{ on }"),
  231. v.VList(
  232. rowMenus...,
  233. ).Dense(true),
  234. ),
  235. ).Style("width: 64px;").Class("pl-0")
  236. } else {
  237. td = h.Td().Style("width: 64px;").Class("pl-0")
  238. }
  239. bindTds = append(bindTds, td)
  240. }
  241. tdCount = len(bindTds)
  242. row := h.Tr(bindTds...)
  243. if b.loadMoreCount > 0 && i >= b.loadMoreCount {
  244. if len(b.loadMoreURL) > 0 {
  245. return
  246. } else {
  247. row.Attr("v-if", fmt.Sprintf("vars.%s", loadMoreVarName))
  248. }
  249. haveMoreRecord = true
  250. }
  251. if b.rowWrapper != nil {
  252. rows = append(rows, b.rowWrapper(row, id, obj, dataTableId))
  253. } else {
  254. rows = append(rows, row)
  255. }
  256. if hasExpand {
  257. rows = append(rows,
  258. h.Tr(
  259. h.Td(
  260. v.VExpandTransition(
  261. h.Div(
  262. b.rowExpandFunc(obj, ctx),
  263. v.VDivider(),
  264. ).Attr("v-if", fmt.Sprintf("vars.%s_%d", expandVarName, i)).
  265. Class("grey lighten-5"),
  266. ),
  267. ).Attr("colspan", fmt.Sprint(tdCount)).Class("pa-0").Style("height: auto; border-bottom: none"),
  268. ).Class("v-data-table__expand-row"),
  269. )
  270. }
  271. i++
  272. })
  273. var thead h.HTMLComponent
  274. if !b.withoutHeaders {
  275. var heads []h.HTMLComponent
  276. if hasExpand {
  277. heads = append(heads, h.Th(" "))
  278. }
  279. if b.selectable {
  280. allInputValue := ""
  281. idsOfPageComma := strings.Join(idsOfPage, ",")
  282. if allSelected(selected, idsOfPage) {
  283. allInputValue = idsOfPageComma
  284. }
  285. onChange := web.Plaid().
  286. PushState(true).
  287. MergeQuery(true).
  288. Query(b.selectionParamName,
  289. web.Var(fmt.Sprintf(`{value: %s, add: $event, remove: !$event}`,
  290. h.JSONString(idsOfPage))),
  291. ).Go()
  292. if b.onSelectAllFunc != nil {
  293. onChange = b.onSelectAllFunc(idsOfPage, ctx)
  294. }
  295. heads = append(heads, h.Th("").Children(
  296. v.VCheckbox().
  297. Class("mt-0").
  298. TrueValue(idsOfPageComma).
  299. InputValue(allInputValue).
  300. HideDetails(true).
  301. Attr("@change", onChange),
  302. ).Style("width: 48px;").Class("pr-0"))
  303. }
  304. for _, f := range b.columns {
  305. var head h.HTMLComponent
  306. th := h.Th(f.title)
  307. head = th
  308. if b.headCellWrapper != nil {
  309. head = b.headCellWrapper(
  310. (h.MutableAttrHTMLComponent)(th),
  311. f.name,
  312. f.title,
  313. )
  314. }
  315. heads = append(heads, head)
  316. }
  317. if hasRowMenuCol {
  318. heads = append(heads, h.Th("").Children(b.selectableColumnsBtn).Style("width: 64px;").Class("pl-0")) // Edit, Delete menu
  319. }
  320. thead = h.Thead(
  321. h.Tr(heads...),
  322. ).Class("grey lighten-5")
  323. }
  324. var tfoot h.HTMLComponent
  325. if len(b.tfootChildren) > 0 {
  326. tfoot = h.Tfoot(b.tfootChildren...)
  327. }
  328. if b.loadMoreCount > 0 && haveMoreRecord {
  329. var btn h.HTMLComponent
  330. if inPlaceLoadMore {
  331. btn = v.VBtn(b.loadMoreLabel).
  332. Text(true).
  333. Small(true).
  334. Class("mt-2").
  335. On("click",
  336. fmt.Sprintf("vars.%s = !vars.%s", loadMoreVarName, loadMoreVarName))
  337. } else {
  338. btn = v.VBtn(b.loadMoreLabel).
  339. Text(true).
  340. Small(true).
  341. Link(true).
  342. Class("mt-2").
  343. Href(b.loadMoreURL)
  344. }
  345. tfoot = h.Tfoot(
  346. h.Tr(
  347. h.Td(
  348. h.If(!hasExpand, v.VDivider()),
  349. btn,
  350. ).Class("text-center pa-0").Attr("colspan", fmt.Sprint(tdCount)),
  351. ),
  352. ).Attr("v-if", fmt.Sprintf("!vars.%s", loadMoreVarName))
  353. }
  354. var selectedCountNotice h.HTMLComponents
  355. onClearSelection := web.Plaid().
  356. MergeQuery(true).
  357. Query(b.selectionParamName, "").
  358. PushState(true).
  359. Go()
  360. if b.onClearSelectionFunc != nil {
  361. onClearSelection = b.onClearSelectionFunc(ctx)
  362. }
  363. {
  364. ss := strings.Split(b.selectedCountLabel, "{count}")
  365. if len(ss) == 1 {
  366. selectedCountNotice = append(selectedCountNotice, h.Text(ss[0]))
  367. } else {
  368. selectedCountNotice = append(selectedCountNotice,
  369. h.Text(ss[0]),
  370. h.Strong(fmt.Sprintf("{{vars.%s}}", selectedCountVarName)),
  371. h.Text(ss[1]),
  372. )
  373. }
  374. }
  375. table := h.Div(
  376. h.Div(
  377. selectedCountNotice,
  378. v.VBtn(b.clearSelectionLabel).
  379. Plain(true).
  380. Text(true).
  381. Small(true).
  382. On("click", onClearSelection),
  383. ).
  384. Class("grey lighten-3 text-center pt-2 pb-2").
  385. Attr("v-show", fmt.Sprintf("vars.%s > 0", selectedCountVarName)),
  386. v.VSimpleTable(
  387. thead,
  388. h.Tbody(rows...),
  389. tfoot,
  390. ),
  391. )
  392. if inPlaceLoadMore {
  393. initContextVarsMap[loadMoreVarName] = false
  394. }
  395. if len(initContextVarsMap) > 0 {
  396. table.Attr(web.InitContextVars, h.JSONString(initContextVarsMap))
  397. }
  398. return table.MarshalHTML(c)
  399. }
  400. func ObjectID(obj interface{}) string {
  401. var id string
  402. if slugger, ok := obj.(primarySlugger); ok {
  403. id = slugger.PrimarySlug()
  404. } else {
  405. id = fmt.Sprint(reflectutils.MustGet(obj, "ID"))
  406. }
  407. return id
  408. }
  409. func getSelectedIds(ctx *web.EventContext, selectedParamName string) (selected []string) {
  410. selectedValue := ctx.R.URL.Query().Get(selectedParamName)
  411. if len(selectedValue) > 0 {
  412. selected = strings.Split(selectedValue, ",")
  413. }
  414. return selected
  415. }
  416. func allSelected(selectedInURL []string, pageSelected []string) bool {
  417. for _, ps := range pageSelected {
  418. if !funk.ContainsString(selectedInURL, ps) {
  419. return false
  420. }
  421. }
  422. return true
  423. }
  424. func (b *DataTableBuilder) Column(name string) (r *DataTableColumnBuilder) {
  425. r = &DataTableColumnBuilder{}
  426. for _, c := range b.columns {
  427. if c.name == name {
  428. return c
  429. }
  430. }
  431. r.Name(name).CellComponentFunc(defaultCellComponentFunc)
  432. b.columns = append(b.columns, r)
  433. return
  434. }
  435. func defaultCellComponentFunc(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
  436. return h.Td(h.Text(fmt.Sprint(reflectutils.MustGet(obj, fieldName))))
  437. }
  438. type DataTableColumnBuilder struct {
  439. name string
  440. title string
  441. cellComponentFunc CellComponentFunc
  442. }
  443. func (b *DataTableColumnBuilder) Name(v string) (r *DataTableColumnBuilder) {
  444. b.name = v
  445. return b
  446. }
  447. func (b *DataTableColumnBuilder) Title(v string) (r *DataTableColumnBuilder) {
  448. b.title = v
  449. return b
  450. }
  451. func (b *DataTableColumnBuilder) CellComponentFunc(v CellComponentFunc) (r *DataTableColumnBuilder) {
  452. b.cellComponentFunc = v
  453. return b
  454. }