123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- package vuetifyx
- import (
- "context"
- "fmt"
- "strings"
- v "github.com/qor5/ui/vuetify"
- "github.com/qor5/web"
- "github.com/rs/xid"
- "github.com/sunfmin/reflectutils"
- h "github.com/theplant/htmlgo"
- "github.com/thoas/go-funk"
- )
- type CellComponentFunc func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent
- type CellWrapperFunc func(cell h.MutableAttrHTMLComponent, id string, obj interface{}, dataTableID string) h.HTMLComponent
- type HeadCellWrapperFunc func(cell h.MutableAttrHTMLComponent, field string, title string) h.HTMLComponent
- type RowWrapperFunc func(row h.MutableAttrHTMLComponent, id string, obj interface{}, dataTableID string) h.HTMLComponent
- type RowMenuItemFunc func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent
- type RowComponentFunc func(obj interface{}, ctx *web.EventContext) h.HTMLComponent
- type OnSelectFunc func(id string, ctx *web.EventContext) string
- type OnSelectAllFunc func(idsOfPage []string, ctx *web.EventContext) string
- type OnClearSelectionFunc func(ctx *web.EventContext) string
- type DataTableBuilder struct {
- data interface{}
- selectable bool
- withoutHeaders bool
- selectionParamName string
- cellWrapper CellWrapperFunc
- headCellWrapper HeadCellWrapperFunc
- rowWrapper RowWrapperFunc
- rowMenuItemFuncs []RowMenuItemFunc
- rowExpandFunc RowComponentFunc
- columns []*DataTableColumnBuilder
- loadMoreCount int
- loadMoreLabel string
- loadMoreURL string
- // e.g. {count} records are selected.
- selectedCountLabel string
- clearSelectionLabel string
- onClearSelectionFunc OnClearSelectionFunc
- tfootChildren []h.HTMLComponent
- selectableColumnsBtn h.HTMLComponent
- onSelectFunc OnSelectFunc
- onSelectAllFunc OnSelectAllFunc
- }
- func DataTable(data interface{}) (r *DataTableBuilder) {
- r = &DataTableBuilder{
- data: data,
- selectionParamName: "selected_ids",
- selectedCountLabel: "{count} records are selected.",
- clearSelectionLabel: "clear selection",
- }
- return
- }
- func (b *DataTableBuilder) LoadMoreAt(count int, label string) (r *DataTableBuilder) {
- b.loadMoreCount = count
- b.loadMoreLabel = label
- return b
- }
- func (b *DataTableBuilder) LoadMoreURL(url string) (r *DataTableBuilder) {
- b.loadMoreURL = url
- return b
- }
- func (b *DataTableBuilder) Tfoot(children ...h.HTMLComponent) (r *DataTableBuilder) {
- b.tfootChildren = children
- return b
- }
- func (b *DataTableBuilder) Selectable(v bool) (r *DataTableBuilder) {
- b.selectable = v
- return b
- }
- func (b *DataTableBuilder) Data(v interface{}) (r *DataTableBuilder) {
- b.data = v
- return b
- }
- func (b *DataTableBuilder) SelectionParamName(v string) (r *DataTableBuilder) {
- b.selectionParamName = v
- return b
- }
- func (b *DataTableBuilder) WithoutHeader(v bool) (r *DataTableBuilder) {
- b.withoutHeaders = v
- return b
- }
- func (b *DataTableBuilder) CellWrapperFunc(v CellWrapperFunc) (r *DataTableBuilder) {
- b.cellWrapper = v
- return b
- }
- func (b *DataTableBuilder) HeadCellWrapperFunc(v HeadCellWrapperFunc) (r *DataTableBuilder) {
- b.headCellWrapper = v
- return b
- }
- func (b *DataTableBuilder) RowWrapperFunc(v RowWrapperFunc) (r *DataTableBuilder) {
- b.rowWrapper = v
- return b
- }
- func (b *DataTableBuilder) RowMenuItemFuncs(vs ...RowMenuItemFunc) (r *DataTableBuilder) {
- b.rowMenuItemFuncs = vs
- return b
- }
- func (b *DataTableBuilder) RowMenuItemFunc(v RowMenuItemFunc) (r *DataTableBuilder) {
- b.rowMenuItemFuncs = append(b.rowMenuItemFuncs, v)
- return b
- }
- func (b *DataTableBuilder) RowExpandFunc(v RowComponentFunc) (r *DataTableBuilder) {
- b.rowExpandFunc = v
- return b
- }
- func (b *DataTableBuilder) SelectedCountLabel(v string) (r *DataTableBuilder) {
- b.selectedCountLabel = v
- return b
- }
- func (b *DataTableBuilder) ClearSelectionLabel(v string) (r *DataTableBuilder) {
- b.clearSelectionLabel = v
- return b
- }
- func (b *DataTableBuilder) OnClearSelectionFunc(v OnClearSelectionFunc) (r *DataTableBuilder) {
- b.onClearSelectionFunc = v
- return b
- }
- func (b *DataTableBuilder) SelectableColumnsBtn(v h.HTMLComponent) (r *DataTableBuilder) {
- b.selectableColumnsBtn = v
- return b
- }
- func (b *DataTableBuilder) OnSelectAllFunc(v OnSelectAllFunc) (r *DataTableBuilder) {
- b.onSelectAllFunc = v
- return b
- }
- func (b *DataTableBuilder) OnSelectFunc(v OnSelectFunc) (r *DataTableBuilder) {
- b.onSelectFunc = v
- return b
- }
- type primarySlugger interface {
- PrimarySlug() string
- }
- func (b *DataTableBuilder) MarshalHTML(c context.Context) (r []byte, err error) {
- ctx := web.MustGetEventContext(c)
- selected := getSelectedIds(ctx, b.selectionParamName)
- dataTableId := xid.New().String()
- loadMoreVarName := fmt.Sprintf("loadmore_%s", dataTableId)
- expandVarName := fmt.Sprintf("expand_%s", dataTableId)
- selectedCountVarName := fmt.Sprintf("selected_count_%s", dataTableId)
- initContextVarsMap := map[string]interface{}{
- selectedCountVarName: len(selected),
- }
- // map[obj_id]{rowMenus}
- objRowMenusMap := make(map[string][]h.HTMLComponent)
- funk.ForEach(b.data, func(obj interface{}) {
- id := ObjectID(obj)
- var opMenuItems []h.HTMLComponent
- for _, f := range b.rowMenuItemFuncs {
- item := f(obj, id, ctx)
- if item == nil {
- continue
- }
- opMenuItems = append(opMenuItems, item)
- }
- if len(opMenuItems) > 0 {
- objRowMenusMap[id] = opMenuItems
- }
- })
- hasRowMenuCol := len(objRowMenusMap) > 0 || b.selectableColumnsBtn != nil
- var rows []h.HTMLComponent
- var idsOfPage []string
- inPlaceLoadMore := b.loadMoreCount > 0 && b.loadMoreURL == ""
- hasExpand := b.rowExpandFunc != nil
- i := 0
- tdCount := 0
- haveMoreRecord := false
- funk.ForEach(b.data, func(obj interface{}) {
- id := ObjectID(obj)
- idsOfPage = append(idsOfPage, id)
- inputValue := ""
- if funk.ContainsString(selected, id) {
- inputValue = id
- }
- var tds []h.HTMLComponent
- if hasExpand {
- initContextVarsMap[fmt.Sprintf("%s_%d", expandVarName, i)] = false
- tds = append(tds, h.Td(
- v.VIcon("$vuetify.icons.expand").
- Attr(":class", fmt.Sprintf("{\"v-data-table__expand-icon--active\": vars.%s_%d, \"v-data-table__expand-icon\": true}", expandVarName, i)).
- On("click", fmt.Sprintf("vars.%s_%d = !vars.%s_%d", expandVarName, i, expandVarName, i)),
- ).Class("pr-0").Style("width: 40px;"))
- }
- if b.selectable {
- onChange := web.Plaid().
- PushState(true).
- MergeQuery(true).
- Query(b.selectionParamName,
- web.Var(fmt.Sprintf(`{value: %s, add: $event, remove: !$event}`, h.JSONString(id))),
- ).RunPushState()
- if b.onSelectFunc != nil {
- onChange = b.onSelectFunc(id, ctx)
- }
- tds = append(tds, h.Td(
- v.VCheckbox().
- Class("mt-0").
- InputValue(inputValue).
- TrueValue(id).
- FalseValue("").
- HideDetails(true).
- Attr("@change", onChange+fmt.Sprintf(";vars.%s+=($event?1:-1)", selectedCountVarName)),
- ).Class("pr-0"))
- }
- for _, f := range b.columns {
- tds = append(tds, f.cellComponentFunc(obj, f.name, ctx))
- }
- var bindTds []h.HTMLComponent
- for _, td := range tds {
- std, ok := td.(h.MutableAttrHTMLComponent)
- if !ok {
- bindTds = append(bindTds, td)
- continue
- }
- var tdWrapped h.HTMLComponent = std
- if b.cellWrapper != nil {
- tdWrapped = b.cellWrapper(std, id, obj, dataTableId)
- }
- bindTds = append(bindTds, tdWrapped)
- }
- if hasRowMenuCol {
- var td h.HTMLComponent
- rowMenus, ok := objRowMenusMap[id]
- if ok {
- td = h.Td(
- v.VMenu(
- web.Slot(
- v.VBtn("").Children(
- v.VIcon("more_horiz"),
- ).Attr("v-on", "on").Text(true).Fab(true).Small(true),
- ).Name("activator").Scope("{ on }"),
- v.VList(
- rowMenus...,
- ).Dense(true),
- ),
- ).Style("width: 64px;").Class("pl-0")
- } else {
- td = h.Td().Style("width: 64px;").Class("pl-0")
- }
- bindTds = append(bindTds, td)
- }
- tdCount = len(bindTds)
- row := h.Tr(bindTds...)
- if b.loadMoreCount > 0 && i >= b.loadMoreCount {
- if len(b.loadMoreURL) > 0 {
- return
- } else {
- row.Attr("v-if", fmt.Sprintf("vars.%s", loadMoreVarName))
- }
- haveMoreRecord = true
- }
- if b.rowWrapper != nil {
- rows = append(rows, b.rowWrapper(row, id, obj, dataTableId))
- } else {
- rows = append(rows, row)
- }
- if hasExpand {
- rows = append(rows,
- h.Tr(
- h.Td(
- v.VExpandTransition(
- h.Div(
- b.rowExpandFunc(obj, ctx),
- v.VDivider(),
- ).Attr("v-if", fmt.Sprintf("vars.%s_%d", expandVarName, i)).
- Class("grey lighten-5"),
- ),
- ).Attr("colspan", fmt.Sprint(tdCount)).Class("pa-0").Style("height: auto; border-bottom: none"),
- ).Class("v-data-table__expand-row"),
- )
- }
- i++
- })
- var thead h.HTMLComponent
- if !b.withoutHeaders {
- var heads []h.HTMLComponent
- if hasExpand {
- heads = append(heads, h.Th(" "))
- }
- if b.selectable {
- allInputValue := ""
- idsOfPageComma := strings.Join(idsOfPage, ",")
- if allSelected(selected, idsOfPage) {
- allInputValue = idsOfPageComma
- }
- onChange := web.Plaid().
- PushState(true).
- MergeQuery(true).
- Query(b.selectionParamName,
- web.Var(fmt.Sprintf(`{value: %s, add: $event, remove: !$event}`,
- h.JSONString(idsOfPage))),
- ).Go()
- if b.onSelectAllFunc != nil {
- onChange = b.onSelectAllFunc(idsOfPage, ctx)
- }
- heads = append(heads, h.Th("").Children(
- v.VCheckbox().
- Class("mt-0").
- TrueValue(idsOfPageComma).
- InputValue(allInputValue).
- HideDetails(true).
- Attr("@change", onChange),
- ).Style("width: 48px;").Class("pr-0"))
- }
- for _, f := range b.columns {
- var head h.HTMLComponent
- th := h.Th(f.title)
- head = th
- if b.headCellWrapper != nil {
- head = b.headCellWrapper(
- (h.MutableAttrHTMLComponent)(th),
- f.name,
- f.title,
- )
- }
- heads = append(heads, head)
- }
- if hasRowMenuCol {
- heads = append(heads, h.Th("").Children(b.selectableColumnsBtn).Style("width: 64px;").Class("pl-0")) // Edit, Delete menu
- }
- thead = h.Thead(
- h.Tr(heads...),
- ).Class("grey lighten-5")
- }
- var tfoot h.HTMLComponent
- if len(b.tfootChildren) > 0 {
- tfoot = h.Tfoot(b.tfootChildren...)
- }
- if b.loadMoreCount > 0 && haveMoreRecord {
- var btn h.HTMLComponent
- if inPlaceLoadMore {
- btn = v.VBtn(b.loadMoreLabel).
- Text(true).
- Small(true).
- Class("mt-2").
- On("click",
- fmt.Sprintf("vars.%s = !vars.%s", loadMoreVarName, loadMoreVarName))
- } else {
- btn = v.VBtn(b.loadMoreLabel).
- Text(true).
- Small(true).
- Link(true).
- Class("mt-2").
- Href(b.loadMoreURL)
- }
- tfoot = h.Tfoot(
- h.Tr(
- h.Td(
- h.If(!hasExpand, v.VDivider()),
- btn,
- ).Class("text-center pa-0").Attr("colspan", fmt.Sprint(tdCount)),
- ),
- ).Attr("v-if", fmt.Sprintf("!vars.%s", loadMoreVarName))
- }
- var selectedCountNotice h.HTMLComponents
- onClearSelection := web.Plaid().
- MergeQuery(true).
- Query(b.selectionParamName, "").
- PushState(true).
- Go()
- if b.onClearSelectionFunc != nil {
- onClearSelection = b.onClearSelectionFunc(ctx)
- }
- {
- ss := strings.Split(b.selectedCountLabel, "{count}")
- if len(ss) == 1 {
- selectedCountNotice = append(selectedCountNotice, h.Text(ss[0]))
- } else {
- selectedCountNotice = append(selectedCountNotice,
- h.Text(ss[0]),
- h.Strong(fmt.Sprintf("{{vars.%s}}", selectedCountVarName)),
- h.Text(ss[1]),
- )
- }
- }
- table := h.Div(
- h.Div(
- selectedCountNotice,
- v.VBtn(b.clearSelectionLabel).
- Plain(true).
- Text(true).
- Small(true).
- On("click", onClearSelection),
- ).
- Class("grey lighten-3 text-center pt-2 pb-2").
- Attr("v-show", fmt.Sprintf("vars.%s > 0", selectedCountVarName)),
- v.VSimpleTable(
- thead,
- h.Tbody(rows...),
- tfoot,
- ),
- )
- if inPlaceLoadMore {
- initContextVarsMap[loadMoreVarName] = false
- }
- if len(initContextVarsMap) > 0 {
- table.Attr(web.InitContextVars, h.JSONString(initContextVarsMap))
- }
- return table.MarshalHTML(c)
- }
- func ObjectID(obj interface{}) string {
- var id string
- if slugger, ok := obj.(primarySlugger); ok {
- id = slugger.PrimarySlug()
- } else {
- id = fmt.Sprint(reflectutils.MustGet(obj, "ID"))
- }
- return id
- }
- func getSelectedIds(ctx *web.EventContext, selectedParamName string) (selected []string) {
- selectedValue := ctx.R.URL.Query().Get(selectedParamName)
- if len(selectedValue) > 0 {
- selected = strings.Split(selectedValue, ",")
- }
- return selected
- }
- func allSelected(selectedInURL []string, pageSelected []string) bool {
- for _, ps := range pageSelected {
- if !funk.ContainsString(selectedInURL, ps) {
- return false
- }
- }
- return true
- }
- func (b *DataTableBuilder) Column(name string) (r *DataTableColumnBuilder) {
- r = &DataTableColumnBuilder{}
- for _, c := range b.columns {
- if c.name == name {
- return c
- }
- }
- r.Name(name).CellComponentFunc(defaultCellComponentFunc)
- b.columns = append(b.columns, r)
- return
- }
- func defaultCellComponentFunc(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
- return h.Td(h.Text(fmt.Sprint(reflectutils.MustGet(obj, fieldName))))
- }
- type DataTableColumnBuilder struct {
- name string
- title string
- cellComponentFunc CellComponentFunc
- }
- func (b *DataTableColumnBuilder) Name(v string) (r *DataTableColumnBuilder) {
- b.name = v
- return b
- }
- func (b *DataTableColumnBuilder) Title(v string) (r *DataTableColumnBuilder) {
- b.title = v
- return b
- }
- func (b *DataTableColumnBuilder) CellComponentFunc(v CellComponentFunc) (r *DataTableColumnBuilder) {
- b.cellComponentFunc = v
- return b
- }
|