mirror of
https://github.com/crater-invoice/crater.git
synced 2026-02-08 03:42:41 -05:00
init crater
This commit is contained in:
120
resources/assets/js/components/base/base-table/components/Pagination.vue
Executable file
120
resources/assets/js/components/base/base-table/components/Pagination.vue
Executable file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<nav v-if="shouldShowPagination">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li :class="{ disabled: pagination.currentPage === 1 }">
|
||||
<a
|
||||
:class="{ disabled: pagination.currentPage === 1 }"
|
||||
@click="pageClicked( pagination.currentPage - 1 )"
|
||||
>
|
||||
<i class="left chevron icon">«</i>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="hasFirst" :class="{ active: isActive(1) }" class="page-item">
|
||||
<a class="page-link" @click="pageClicked(1)">1</a>
|
||||
</li>
|
||||
<li v-if="hasFirstEllipsis"><span class="pagination-ellipsis">…</span></li>
|
||||
<li v-for="page in pages" :key="page" :class="{ active: isActive(page), disabled: page === '...' }" class="page-item">
|
||||
<a class="page-link" @click="pageClicked(page)">{{ page }}</a>
|
||||
</li>
|
||||
<li v-if="hasLastEllipsis"><span class="pagination-ellipsis">…</span></li>
|
||||
<li
|
||||
v-if="hasLast"
|
||||
:class="{ active: isActive(this.pagination.totalPages) }"
|
||||
class="page-item"
|
||||
>
|
||||
<a class="page-link" @click="pageClicked(pagination.totalPages)">
|
||||
{{ pagination.totalPages }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
:class="{ disabled: pagination.currentPage === pagination.totalPages }"
|
||||
@click="pageClicked( pagination.currentPage + 1 )"
|
||||
>
|
||||
<i class="right chevron icon">»</i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
pagination: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
pages () {
|
||||
return this.pagination.totalPages === undefined
|
||||
? []
|
||||
: this.pageLinks()
|
||||
},
|
||||
|
||||
hasFirst () {
|
||||
return this.pagination.currentPage >= 4 || this.pagination.totalPages < 10
|
||||
},
|
||||
|
||||
hasLast () {
|
||||
return this.pagination.currentPage <= this.pagination.totalPages - 3 || this.pagination.totalPages < 10
|
||||
},
|
||||
|
||||
hasFirstEllipsis () {
|
||||
return this.pagination.currentPage >= 4 && this.pagination.totalPages >= 10
|
||||
},
|
||||
|
||||
hasLastEllipsis () {
|
||||
return this.pagination.currentPage <= this.pagination.totalPages - 3 && this.pagination.totalPages >= 10
|
||||
},
|
||||
|
||||
shouldShowPagination () {
|
||||
if (this.pagination.totalPages === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.pagination.count === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.pagination.totalPages > 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isActive (page) {
|
||||
const currentPage = this.pagination.currentPage || 1
|
||||
|
||||
return currentPage === page
|
||||
},
|
||||
pageClicked (page) {
|
||||
if (page === '...' ||
|
||||
page === this.pagination.currentPage ||
|
||||
page > this.pagination.totalPages ||
|
||||
page < 1) {
|
||||
return
|
||||
}
|
||||
this.$emit('pageChange', page)
|
||||
},
|
||||
|
||||
pageLinks () {
|
||||
const pages = []
|
||||
|
||||
let left = 2
|
||||
let right = this.pagination.totalPages - 1
|
||||
|
||||
if (this.pagination.totalPages >= 10) {
|
||||
left = Math.max(1, this.pagination.currentPage - 2)
|
||||
right = Math.min(this.pagination.currentPage + 2, this.pagination.totalPages)
|
||||
}
|
||||
for (let i = left; i <= right; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
24
resources/assets/js/components/base/base-table/components/TableCell.js
Executable file
24
resources/assets/js/components/base/base-table/components/TableCell.js
Executable file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
functional: true,
|
||||
|
||||
props: ['column', 'row', 'responsiveLabel'],
|
||||
|
||||
render (createElement, { props }) {
|
||||
const data = {}
|
||||
|
||||
if (props.column.cellClass) {
|
||||
data.class = props.column.cellClass
|
||||
}
|
||||
|
||||
if (props.column.template) {
|
||||
return createElement('td', data, props.column.template(props.row.data))
|
||||
}
|
||||
|
||||
data.domProps = {}
|
||||
data.domProps.innerHTML = props.column.formatter(props.row.getValue(props.column.show), props.row.data)
|
||||
|
||||
return createElement('td', [
|
||||
createElement('span', props.responsiveLabel), data.domProps.innerHTML
|
||||
])
|
||||
}
|
||||
}
|
||||
32
resources/assets/js/components/base/base-table/components/TableColumn.vue
Executable file
32
resources/assets/js/components/base/base-table/components/TableColumn.vue
Executable file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<!-- Never render the contents -->
|
||||
<!-- The scoped slot won't have the required data -->
|
||||
<div v-if="false">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import settings from '../settings'
|
||||
export default {
|
||||
props: {
|
||||
show: { required: false, type: String },
|
||||
label: { default: null, type: String },
|
||||
dataType: { default: 'string', type: String },
|
||||
|
||||
sortable: { default: true, type: Boolean },
|
||||
sortBy: { default: null },
|
||||
|
||||
filterable: { default: true, type: Boolean },
|
||||
sortAs: { default: null },
|
||||
filterOn: { default: null },
|
||||
|
||||
formatter: { default: v => v, type: Function },
|
||||
|
||||
hidden: { default: false, type: Boolean },
|
||||
|
||||
cellClass: { default: settings.cellClass },
|
||||
headerClass: { default: settings.headerClass },
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<th
|
||||
v-if="this.isVisible"
|
||||
slot-scope="col"
|
||||
:aria-sort="ariaSort"
|
||||
:aria-disabled="ariaDisabled"
|
||||
:class="headerClass"
|
||||
role="columnheader"
|
||||
@click="clicked"
|
||||
>
|
||||
{{ label }}
|
||||
</th>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { classList } from '../helpers'
|
||||
export default {
|
||||
props: ['column', 'sort'],
|
||||
|
||||
computed: {
|
||||
ariaDisabled () {
|
||||
if (!this.column.isSortable()) {
|
||||
return 'true'
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
ariaSort () {
|
||||
if (!this.column.isSortable ()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ((this.column.sortAs || this.column.show) !== this.sort.fieldName) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
return this.sort.order === 'asc' ? 'ascending' : 'descending';
|
||||
},
|
||||
|
||||
headerClass () {
|
||||
if (!this.column.isSortable()) {
|
||||
return classList('table-component__th', this.column.headerClass);
|
||||
}
|
||||
|
||||
if ((this.column.sortAs || this.column.show) !== this.sort.fieldName) {
|
||||
return classList('table-component__th table-component__th--sort', this.column.headerClass);
|
||||
}
|
||||
|
||||
return classList(`table-component__th table-component__th--sort-${this.sort.order}`, this.column.headerClass);
|
||||
},
|
||||
|
||||
isVisible () {
|
||||
return !this.column.hidden
|
||||
},
|
||||
|
||||
label () {
|
||||
if (this.column.label === null) {
|
||||
return this.column.show
|
||||
}
|
||||
return this.column.label
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clicked () {
|
||||
if (this.column.isSortable()) {
|
||||
this.$emit('click', this.column)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
330
resources/assets/js/components/base/base-table/components/TableComponent.vue
Executable file
330
resources/assets/js/components/base/base-table/components/TableComponent.vue
Executable file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="table-component">
|
||||
<div v-if="showFilter && filterableColumnExists" class="table-component__filter">
|
||||
<input
|
||||
:class="fullFilterInputClass"
|
||||
v-model="filter"
|
||||
:placeholder="filterPlaceholder"
|
||||
type="text"
|
||||
>
|
||||
<a v-if="filter" class="table-component__filter__clear" @click="filter = ''">×</a>
|
||||
</div>
|
||||
|
||||
<div class="table-component__table-wrapper">
|
||||
<base-loader v-if="loading" class="table-loader" />
|
||||
|
||||
<table :class="fullTableClass">
|
||||
<caption
|
||||
v-if="showCaption"
|
||||
class="table-component__table__caption"
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
>{{ ariaCaption }}</caption>
|
||||
<thead :class="fullTableHeadClass">
|
||||
<tr>
|
||||
<table-column-header
|
||||
v-for="column in columns"
|
||||
:key="column.show || column.show"
|
||||
:sort="sort"
|
||||
:column="column"
|
||||
@click="changeSorting"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody :class="fullTableBodyClass">
|
||||
<table-row
|
||||
v-for="row in displayedRows"
|
||||
:key="row.vueTableComponentInternalRowId"
|
||||
:row="row"
|
||||
:columns="columns"
|
||||
@rowClick="emitRowClick"
|
||||
/>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<slot :rows="rows" name="tfoot" />
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="displayedRows.length === 0 && !loading" class="table-component__message">{{ filterNoResults }}</div>
|
||||
|
||||
<div style="display:none;">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<pagination v-if="pagination && !loading" :pagination="pagination" @pageChange="pageChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Column from '../classes/Column'
|
||||
import expiringStorage from '../expiring-storage'
|
||||
import Row from '../classes/Row'
|
||||
import TableColumnHeader from './TableColumnHeader'
|
||||
import TableRow from './TableRow'
|
||||
import settings from '../settings'
|
||||
import Pagination from './Pagination'
|
||||
import { classList, pick } from '../helpers'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TableColumnHeader,
|
||||
TableRow,
|
||||
Pagination
|
||||
},
|
||||
|
||||
props: {
|
||||
data: { default: () => [], type: [Array, Function] },
|
||||
|
||||
showFilter: { type: Boolean, default: true },
|
||||
showCaption: { type: Boolean, default: true },
|
||||
|
||||
sortBy: { default: '', type: String },
|
||||
sortOrder: { default: '', type: String },
|
||||
|
||||
cacheKey: { default: null },
|
||||
cacheLifetime: { default: 5 },
|
||||
|
||||
tableClass: { default: () => settings.tableClass },
|
||||
theadClass: { default: () => settings.theadClass },
|
||||
tbodyClass: { default: () => settings.tbodyClass },
|
||||
filterInputClass: { default: () => settings.filterInputClass },
|
||||
filterPlaceholder: { default: () => settings.filterPlaceholder },
|
||||
filterNoResults: { default: () => settings.filterNoResults }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
columns: [],
|
||||
rows: [],
|
||||
filter: '',
|
||||
sort: {
|
||||
fieldName: '',
|
||||
order: ''
|
||||
},
|
||||
pagination: null,
|
||||
|
||||
loading: false,
|
||||
localSettings: {}
|
||||
}),
|
||||
|
||||
computed: {
|
||||
fullTableClass () {
|
||||
return classList('table-component__table', this.tableClass)
|
||||
},
|
||||
|
||||
fullTableHeadClass () {
|
||||
return classList('table-component__table__head', this.theadClass)
|
||||
},
|
||||
|
||||
fullTableBodyClass () {
|
||||
return classList('table-component__table__body', this.tbodyClass)
|
||||
},
|
||||
|
||||
fullFilterInputClass () {
|
||||
return classList('table-component__filter__field', this.filterInputClass)
|
||||
},
|
||||
|
||||
ariaCaption () {
|
||||
if (this.sort.fieldName === '') {
|
||||
return 'Table not sorted'
|
||||
}
|
||||
|
||||
return (
|
||||
`Table sorted by ${this.sort.fieldName} ` +
|
||||
(this.sort.order === 'asc' ? '(ascending)' : '(descending)')
|
||||
)
|
||||
},
|
||||
|
||||
usesLocalData () {
|
||||
return Array.isArray(this.data)
|
||||
},
|
||||
|
||||
displayedRows () {
|
||||
if (!this.usesLocalData) {
|
||||
return this.sortedRows
|
||||
}
|
||||
|
||||
if (!this.showFilter) {
|
||||
return this.sortedRows
|
||||
}
|
||||
|
||||
if (!this.columns.filter(column => column.isFilterable()).length) {
|
||||
return this.sortedRows
|
||||
}
|
||||
|
||||
return this.sortedRows.filter(row => row.passesFilter(this.filter))
|
||||
},
|
||||
|
||||
sortedRows () {
|
||||
if (!this.usesLocalData) {
|
||||
return this.rows
|
||||
}
|
||||
|
||||
if (this.sort.fieldName === '') {
|
||||
return this.rows
|
||||
}
|
||||
|
||||
if (this.columns.length === 0) {
|
||||
return this.rows
|
||||
}
|
||||
|
||||
const sortColumn = this.getColumn(this.sort.fieldName)
|
||||
|
||||
if (!sortColumn) {
|
||||
return this.rows
|
||||
}
|
||||
|
||||
return this.rows.sort(
|
||||
sortColumn.getSortPredicate(this.sort.order, this.columns)
|
||||
)
|
||||
},
|
||||
|
||||
filterableColumnExists () {
|
||||
return this.columns.filter(c => c.isFilterable()).length > 0
|
||||
},
|
||||
|
||||
storageKey () {
|
||||
return this.cacheKey
|
||||
? `vue-table-component.${this.cacheKey}`
|
||||
: `vue-table-component.${window.location.host}${window.location.pathname}${this.cacheKey}`
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
filter () {
|
||||
if (!this.usesLocalData) {
|
||||
this.mapDataToRows()
|
||||
}
|
||||
|
||||
this.saveState()
|
||||
},
|
||||
|
||||
data () {
|
||||
if (this.usesLocalData) {
|
||||
this.mapDataToRows()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.sort.order = this.sortOrder
|
||||
|
||||
this.restoreState()
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
this.sort.fieldName = this.sortBy
|
||||
const columnComponents = this.$slots.default
|
||||
.filter(column => column.componentInstance)
|
||||
.map(column => column.componentInstance)
|
||||
|
||||
this.columns = columnComponents.map(column => new Column(column))
|
||||
|
||||
columnComponents.forEach(columnCom => {
|
||||
Object.keys(columnCom.$options.props).forEach(prop =>
|
||||
columnCom.$watch(prop, () => {
|
||||
this.columns = columnComponents.map(column => new Column(column))
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
await this.mapDataToRows()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async pageChange (page) {
|
||||
this.pagination.currentPage = page
|
||||
|
||||
await this.mapDataToRows()
|
||||
},
|
||||
|
||||
async mapDataToRows () {
|
||||
const data = this.usesLocalData
|
||||
? this.prepareLocalData()
|
||||
: await this.fetchServerData()
|
||||
|
||||
let rowId = 0
|
||||
|
||||
this.rows = data
|
||||
.map(rowData => {
|
||||
rowData.vueTableComponentInternalRowId = rowId++
|
||||
return rowData
|
||||
})
|
||||
.map(rowData => new Row(rowData, this.columns))
|
||||
},
|
||||
|
||||
prepareLocalData () {
|
||||
this.pagination = null
|
||||
|
||||
return this.data
|
||||
},
|
||||
|
||||
async fetchServerData () {
|
||||
const page = (this.pagination && this.pagination.currentPage) || 1
|
||||
this.loading = true
|
||||
|
||||
const response = await this.data({
|
||||
filter: this.filter,
|
||||
sort: this.sort,
|
||||
page: page
|
||||
})
|
||||
|
||||
this.pagination = response.pagination
|
||||
this.loading = false
|
||||
return response.data
|
||||
},
|
||||
|
||||
async refresh () {
|
||||
if (this.pagination) {
|
||||
this.pagination.currentPage = 1
|
||||
}
|
||||
await this.mapDataToRows()
|
||||
},
|
||||
|
||||
changeSorting (column) {
|
||||
if (this.sort.fieldName !== (column.sortAs || column.show)) {
|
||||
this.sort.fieldName = (column.sortAs || column.show)
|
||||
this.sort.order = 'asc'
|
||||
} else {
|
||||
this.sort.order = this.sort.order === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
if (!this.usesLocalData) {
|
||||
this.mapDataToRows()
|
||||
}
|
||||
|
||||
this.saveState()
|
||||
},
|
||||
|
||||
getColumn (columnName) {
|
||||
return this.columns.find(column => column.show === columnName)
|
||||
},
|
||||
|
||||
saveState () {
|
||||
expiringStorage.set(
|
||||
this.storageKey,
|
||||
pick(this.$data, ['filter', 'sort']),
|
||||
this.cacheLifetime
|
||||
)
|
||||
},
|
||||
|
||||
restoreState () {
|
||||
const previousState = expiringStorage.get(this.storageKey)
|
||||
|
||||
if (previousState === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sort = previousState.sort
|
||||
this.filter = previousState.filter
|
||||
|
||||
this.saveState()
|
||||
},
|
||||
|
||||
emitRowClick (row) {
|
||||
this.$emit('rowClick', row)
|
||||
this.$emit('row-click', row)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
38
resources/assets/js/components/base/base-table/components/TableRow.vue
Executable file
38
resources/assets/js/components/base/base-table/components/TableRow.vue
Executable file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<tr @click="onClick">
|
||||
<table-cell
|
||||
v-for="column in visibleColumns"
|
||||
:row="row"
|
||||
:column="column"
|
||||
:key="column.id"
|
||||
:responsive-label="column.label"
|
||||
></table-cell>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TableCell from './TableCell';
|
||||
|
||||
export default {
|
||||
props: ['columns', 'row'],
|
||||
|
||||
components: {
|
||||
TableCell,
|
||||
},
|
||||
|
||||
computed: {
|
||||
visibleColumns() {
|
||||
return this.columns.filter(column => ! column.hidden);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick(e) {
|
||||
this.$emit('rowClick', {
|
||||
e,
|
||||
row: this.row
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user