init crater

This commit is contained in:
Mohit Panjwani
2019-11-11 12:16:00 +05:30
commit bdf2ba51d6
668 changed files with 158503 additions and 0 deletions

View File

@ -0,0 +1,64 @@
import { pick } from '../helpers'
export default class Column {
constructor (columnComponent) {
const properties = pick(columnComponent, [
'show', 'label', 'dataType', 'sortable', 'sortBy', 'filterable',
'filterOn', 'hidden', 'formatter', 'cellClass', 'headerClass', 'sortAs'
])
for (const property in properties) {
this[property] = columnComponent[property]
}
this.template = columnComponent.$scopedSlots.default
}
isFilterable () {
return this.filterable
}
getFilterFieldName () {
return this.filterOn || this.show
}
isSortable () {
return this.sortable
}
getSortPredicate (sortOrder, allColumns) {
const sortFieldName = this.getSortFieldName()
const sortColumn = allColumns.find(column => (column.sortAs === sortFieldName || column.show === sortFieldName))
const dataType = sortColumn.dataType
if (dataType.startsWith('date') || dataType === 'numeric') {
return (row1, row2) => {
const value1 = row1.getSortableValue(sortFieldName)
const value2 = row2.getSortableValue(sortFieldName)
if (sortOrder === 'desc') {
return value2 < value1 ? -1 : 1
}
return value1 < value2 ? -1 : 1
}
}
return (row1, row2) => {
const value1 = row1.getSortableValue(sortFieldName)
const value2 = row2.getSortableValue(sortFieldName)
if (sortOrder === 'desc') {
return value2.localeCompare(value1)
}
return value1.localeCompare(value2)
}
}
getSortFieldName () {
return this.sortBy || this.sortAs || this.show
}
}

View File

@ -0,0 +1,61 @@
import moment from 'moment'
import { get } from '../helpers'
export default class Row {
constructor (data, columns) {
this.data = data
this.columns = columns
}
getValue (columnName) {
return get(this.data, columnName)
}
getColumn (columnName) {
return this.columns.find(column => (column.show === columnName || column.sortAs === columnName))
}
getFilterableValue (columnName) {
const value = this.getValue(columnName)
if (!value) {
return ''
}
return value.toString().toLowerCase()
}
getSortableValue (columnName) {
const dataType = this.getColumn(columnName).dataType
let value = this.getValue(columnName)
if (value === undefined || value === null) {
return ''
}
if (value instanceof String) {
value = value.toLowerCase()
}
if (dataType.startsWith('date')) {
const format = dataType.replace('date:', '')
return moment(value, format).format('YYYYMMDDHHmmss')
}
if (dataType === 'numeric') {
return value
}
return value.toString()
}
passesFilter (filter) {
return this.columns
.filter(column => column.isFilterable())
.map(column => this.getFilterableValue(column.getFilterFieldName()))
.filter(filterableValue => filterableValue.indexOf(filter.toLowerCase()) >= 0)
.length
}
}

View 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">&hellip;</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">&hellip;</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>

View 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
])
}
}

View 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>

View File

@ -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>

View 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>

View 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>

View File

@ -0,0 +1,34 @@
class ExpiringStorage {
get (key) {
const cached = JSON.parse(
localStorage.getItem(key)
)
if (!cached) {
return null
}
const expires = new Date(cached.expires)
if (expires < new Date()) {
localStorage.removeItem(key)
return null
}
return cached.value
}
has (key) {
return this.get(key) !== null
}
set (key, value, lifeTimeInMinutes) {
const currentTime = new Date().getTime()
const expires = new Date(currentTime + lifeTimeInMinutes * 60000)
localStorage.setItem(key, JSON.stringify({ value, expires }))
}
}
export default new ExpiringStorage()

View File

@ -0,0 +1,30 @@
export function classList (...classes) {
return classes
.map(c => Array.isArray(c) ? c : [c])
.reduce((classes, c) => classes.concat(c), [])
}
export function get (object, path) {
if (!path) {
return object
}
if (object === null || typeof object !== 'object') {
return object
}
const [pathHead, pathTail] = path.split(/\.(.+)/)
return get(object[pathHead], pathTail)
}
export function pick (object, properties) {
return properties.reduce((pickedObject, property) => {
pickedObject[property] = object[property]
return pickedObject
}, {})
}
export function range (from, to) {
return [...Array(to - from)].map((_, i) => i + from)
}

View File

@ -0,0 +1,20 @@
import TableComponent from './components/TableComponent'
import TableColumn from './components/TableColumn'
import Pagination from './components/Pagination'
import { mergeSettings } from './settings'
export default {
install (Vue, options = {}) {
mergeSettings(options)
Vue.component('table-component', TableComponent)
Vue.component('table-column', TableColumn)
Vue.component('pagination', Pagination)
},
settings (settings) {
mergeSettings(settings)
}
}
export { TableComponent, TableColumn }

View File

@ -0,0 +1,18 @@
const settings = {
tableClass: '',
theadClass: '',
tbodyClass: '',
headerClass: '',
cellClass: '',
filterInputClass: '',
filterPlaceholder: 'Filter table…',
filterNoResults: 'There are no matching rows'
}
export function mergeSettings (newSettings) {
for (const setting in newSettings) {
settings[setting] = newSettings[setting]
}
}
export default settings