<template>
<div
:id="id"
ref="root"
class="datatable"
style="position: relative"
:class="{
selectable: !!select,
'loader-overlay': isDatatableLoading && isDatatableLoading.value,
}"
:data-name="name"
>
<div
v-if="isDatatableLoading && isDatatableLoading.value"
class="loader-wrapper"
>
<b-spinner
class="loader"
variant="info"
style="width: 3rem; height: 3rem; margin: 0 auto; display: block"
></b-spinner>
</div>
<div v-if="ssrPaging" class="datatable_toolbar">
<div>
<label>Afficher</label>
<select v-model="pageLength" class="dataTable_cmp__rows_select">
<option>10</option>
<option>20</option>
<option>50</option>
<option>100</option>
</select>
<label>éléments</label>
</div>
<div v-show="ssrPaging && !ssrShowMultiColumnFilter" />
<div
v-show="ssrPaging && ssrShowMultiColumnFilter"
class="datatable_filter"
>
<label class="datatable_filters__input__label"> Filtrer: </label>
<!--- TODO: Built-in filter input -->
<input class="datatable_filters__input" />
</div>
</div>
<table ref="table" class="dataTable__table display compact stripe" />
<DataTablePaginator
v-if="ssrPaging === true"
ref="paginator"
:page-length="pageLen"
:name="name"
/>
</div>
</template>
<script>
import Vue from 'vue'
import $ from 'jquery'
import fr from '@/i18n/i18n.fr'
const defaultLanguage = fr.datatable
const R = require('ramda')
import DataTablePaginator from './DataTablePaginator'
require('datatables.net')
require('datatables.net-select-dt')
import { mapGetters } from 'vuex'
require('@/libs/datetime-moment')
const initDelay = 1000
var dynamicComponents = Vue.__DataTableDynamicComponents || {}
Vue.__DataTableDynamicComponents = dynamicComponents
var dataWatchFlags = {}
export function toggleDataWatch(name, shouldWatch = true) {
dataWatchFlags[name] = shouldWatch
Vue.$log.debug('toggleDataWatch', name, shouldWatch, {
dataWatchFlags,
})
}
/**
* Allow you to dynamically render a Vue component inside a Datatable cell
*
* Available properties:
* row Object
* datatable Function
* vueDatatable Object
*
* Usage:
* columnDefs: [
* {
* targets: 0,
* orderable: false,
* render: createComponentRender({
* name: "MagnifyingGlass",
* template: `
* <StatusIcon :row="row" ></StatusIcon>
* `,
* }),
* },
* @namespace components
* @category components
* @subcategory shared/Datatable
* @module DataTable
*/
export function createComponentRender(componentDefinition = {}) {
componentDefinition.props = {
row: Object,
datatable: Function,
vueDatatable: Object,
}
dynamicComponents[componentDefinition.name] = componentDefinition
return (data = '', type = '', row = {}) => {
let dataRow = ''
try {
dataRow = btoa(encodeURI(JSON.stringify(row)))
} catch (err) {
Vue.$log.debug('datatable:fail_to_decode_row', {
row,
})
throw err
}
return `<div data-needs-vue-rendering=1 data-component="${componentDefinition.name}" data-value="${data}"
data-type="${type}"
data-row="${dataRow}"
></div>`
}
}
export function registerDatatableComponent(name, def = {}) {
if (typeof def === 'function') {
let dynamic = def
def = {
components: {
[name + 'Inner']: dynamic,
},
template: `
<${name + 'Inner'}/>
`,
}
}
def.name = name
def.props = {
row: Object,
datatable: Function,
vueDatatable: Object,
}
dynamicComponents[name] = def
}
export default {
components: {
DataTablePaginator,
},
inject: {
disableDatatableSetHeightFromParent: {
default: null,
},
isDatatableLoading: {
default: null, //{value: true/false}
},
},
props: {
columns: {
type: Array,
default: () => [],
},
rowId: {
type: String,
default: '',
},
columnDefs: {
type: Array,
default: () => [],
},
name: {
type: String,
required: true,
},
paging: Boolean,
ssrPaging: Boolean,
searching: Boolean,
ssrColumnOrderingMiddleware: {
type: Function,
default: () => null,
},
language: {
type: Object,
default: () => ({}),
},
ssrSortingCooldownDuration: {
type: Number,
default: 0,
},
defaultSortingColumn: {
type: Number,
default: null,
},
defaultSortingColumnDirection: {
type: String,
default: 'asc',
},
scrollY: {
type: [String, Boolean],
default: false,
},
scroller: Boolean,
ssrShowMultiColumnFilter: Boolean,
select: {
type: [Object, Boolean],
default: false,
},
extraOptions: {
type: Object,
default: () => ({}),
},
autoHeight: {
type: Boolean,
default: false,
},
autoHeightOffset: {
type: Number,
default: 0,
},
},
data() {
return {
initialized: false,
id: `datatable_${btoa(Math.random()).substring(0, 12)}`,
pageLength: this.extraOptions.pageLength || 10,
datatable: null,
ssrSortingCooldown: null,
}
},
computed: {
...mapGetters({
getTableInfos: 'datatable/getTableInfos',
getTableItems: 'datatable/getTableItems',
appDynamicLayout: 'app/layout',
}),
currentPage() {
return this.getTableInfos(this.name).currentPage || 1
},
pageLen() {
return this.datatable && this.datatable.page.len()
},
data() {
return this.getTableItems(this.name)
},
},
watch: {
/**
* @todo: Ideally, this wrapper shouldn't know about the app layout. But app layout changes requires datatable to redraw-itself (column offset issue)
*/
appDynamicLayout: {
handler() {
setTimeout(() => {
this.$nextTick(() => {
this.draw()
})
}, 600)
},
deep: true,
},
data: {
handler(data) {
if (dataWatchFlags[this.name] === false) {
return
}
/*this.$log.debug(
"datatable.watch.data",
this.name,
"dataWatchFlag",
dataWatchFlags[this.name]
);*/
this.onDataChange(data)
},
deep: true,
},
pageLength() {
this.datatable && this.datatable.page.len(this.pageLength)
this.$refs.paginator &&
this.$refs.paginator.$emit('paginateToPage', {
page: 1,
itemsPerPage: this.pageLength,
})
},
},
mounted() {
//this.$log.debug("datatable.mounted", this.name);
this.cooldownSsrSorting()
this.redrawOnWindowResizeBind()
this.$nextTick(() => {
this.onDataChange(this.data)
})
if (this.autoHeight) {
$(window).resize(this.setHeightFromParent)
}
let self = this
function waitForInitialized(cb) {
if (self.initialized) {
cb()
} else {
self.waitForInitializedTimeout = setTimeout(
() => waitForInitialized(cb),
200
)
}
}
//Issue: Initialization might occurs before completely mounting the component (datatable rendering issue: horizontal scrollbar, columns, offets, etc)
//Solution:Wait for initialized flag, then re-draw after one second
waitForInitialized(() => {
this.waitForInitializedTimeout = setTimeout(() => {
this.datatable.draw()
}, 1000)
})
},
destroyed() {
$(window).off('resize', this.redraw)
if (this.autoHeight) {
$(window).off('resize', this.setHeightFromParent)
}
if (this.waitForInitializedTimeout) {
clearTimeout(this.waitForInitializedTimeout)
}
},
methods: {
setHeightFromParent() {
if (this.disableDatatableSetHeightFromParent === true) {
return
}
if (
this._isDestroyed ||
!this.$refs.root ||
!this.$refs.root.parentNode
) {
return //Skip if destroyed
}
if (
!!this.$refs.root &&
$(this.$refs.root).find('.dataTables_scrollHead').length === 0
) {
this.$log.debug('setHeightFromParent::skip')
return
}
const self = this
let otherParentChildsHeight = $(this.$refs.root.parentNode)
.children()
.toArray()
.filter(function (item) {
return (
$(item).attr('id') !== self.id && item.dataset.autoheight !== '0'
)
})
.map((item) => {
return $(item).height()
})
.reduce((a, v) => a + v, 0)
$(this.$refs.root).find('.dataTables_scrollBody').css('height', `${0}px`)
let tableBaseHeight = $(this.$refs.root)
.find('.dataTables_wrapper')
.height()
let maxHeight = $(this.$refs.root.parentNode).height()
if ($(this.$refs.root.parentNode.parentNode).height() > maxHeight) {
maxHeight = $(this.$refs.root.parentNode.parentNode).height()
}
let height =
maxHeight -
(otherParentChildsHeight +
(this.autoHeightOffset || 0) +
tableBaseHeight)
$(this.$refs.root)
.find('.dataTables_scrollBody')
.css({
height: `${height}px`,
'max-height': `${height}px`,
})
/*this.$log.debug("setHeightFromParent::compute", {
maxHeight,
otherParentChildsHeight,
autoHeightOffset: this.autoHeightOffset,
tableBaseHeight,
height,
});*/
},
setOptions(options = {}) {
$(`#${this.id} table`).DataTable({
...this.getOptions(),
...options,
destroy: true,
})
},
redrawOnWindowResizeBind() {
this.redraw = () => {
this.$nextTick(() => {
this.draw()
})
}
$(window).on('resize', this.redraw)
},
draw() {
//this.$log.debug("datatable.draw", this.name);
this.cooldownSsrSorting()
this.$emit('draw', {
name: this.name,
})
this.datatable.draw()
},
cooldownSsrSorting() {
if (this.ssrPaging) {
this.ssrSortingCooldown = Date.now()
}
},
getDataTable() {
return this.datatable
},
onDataChange() {
this.$emit('change', this.name)
if (this.datatable === null) {
this.initializeDataTable()
}
this.cooldownSsrSorting()
this.datatable.clear().rows.add(this.data || [])
this.draw()
},
getColumns() {
if (this.columns) {
return this.columns
}
if (this.labels) {
return Object.keys(this.labels)
}
},
getOptions() {
let record = this.data[0]
let columns = this.getColumns() || Object.keys(record)
//Warning: defaultLanguage is undefined (new i18n object is a plain object instead of a nested object). See i18n/index.js
let language = {
...defaultLanguage,
...(this.language || {}),
}
delete language.emptyTable
return {
initComplete: () => {
this.onDraw()
},
dom: 'lftipr',
ordering: true,
autoWidth: true,
language,
scrollX: true,
searching: this.searching === true && !this.ssrPaging,
scrollY: this.scrollY !== undefined ? this.scrollY : false,
scroller: this.scroller !== undefined ? this.scroller : false,
deferRender: window.innerHeight > 900 ? true : false,
paging: this.paging === true && !this.ssrPaging,
lengthChange: true,
info: false,
...((this.select && { select: this.select }) || {}),
rowId: this.rowId || 'id',
columnDefs: (this.columnDefs && R.clone(this.columnDefs)) || undefined,
data: this.data,
columns: columns.map((c) => {
const capitalize = (text, defaultValue = '') =>
text
? text.charAt(0).toUpperCase() + text.substring(1)
: defaultValue
const getValue = (obj, key, defaultValue) =>
obj[key] !== undefined ? obj[key] : defaultValue
const isObj = () => typeof c === 'object'
if (!isObj() && c.indexOf('::') !== -1) {
c = {
data: c.split('::')[0],
title: this.$t(c.split('::')[1]),
}
}
c = {
data: typeof c === 'string' ? c : undefined,
...(isObj() ? c : {}),
title:
this.labels && this.labels[c] !== undefined
? this.labels[c]
: (!isObj() && capitalize(c)) ||
(isObj() && getValue(c, 'title', getValue(c, 'label'))),
}
return c
}),
...(this.extraOptions || {}),
}
},
/**
* @warn This doesn't work well with column filters
*/
toggleColumnVisible(index, value) {
if (this.datatable) {
this.datatable.column(index).visible(value)
this.datatable.columns.adjust().draw(false)
}
},
initializeDataTable() {
/*this.$log.debug(
"initializeDataTable",
this.name,
$(`#${this.id} table`).length
);*/
let self = this
$.fn.dataTable && $.fn.dataTable.moment(this.$date.getDatePattern())
if (!$.fn.dataTable) {
this.$log.warn('Datatable::moment plugin not available')
}
this.datatable = $(`#${this.id} table`).DataTable(this.getOptions())
this.datatable.on('draw.dt', (e) => {
e.stopPropagation()
//let wrapperId = $(e.target).closest(`[data-name="${this.name}"]`).get(0).id;
//this.$log.debug("datatable.on.draw", this.name);
this.onDraw()
})
this.datatable.on('order.dt', async (e) => {
e.stopPropagation()
//let wrapperId = $(e.target).closest(`[data-name="${this.name}"]`).get(0).id;
//this.$log.debug("datatable.on.order", this.name);
if (this.ssrPaging) {
if (
this.ssrSortingCooldown != null &&
Date.now() - this.ssrSortingCooldown <
(this.ssrSortingCooldownDuration || 1000)
) {
this.$log.debug('sorting update skip (ssr)')
return
} else {
this.ssrSortingCooldown = Date.now()
}
let fetchOptions = {
name: this.name,
sort: this.datatable.order()[0],
}
if (
this.ssrColumnOrderingMiddleware &&
!this.ssrColumnOrderingMiddleware(fetchOptions)
) {
this.$log.debug('Skip column ordering (SSR)')
return
} else {
this.$log.debug('Sorting...')
}
this.$log.debug('DataTable.$loader.show', this.name)
this.$loader && this.$loader.show()
await this.$store.dispatch('datatable/fetchTableItems', fetchOptions)
this.$log.debug('DataTable.$loader.hide', this.name)
this.$loader && this.$loader.hide()
}
this.$emit('order', {
name: this.name,
})
})
this.datatable.on('select.dt', function (e, dt, type, indexes) {
let payload = {
e,
dt,
type,
indexes,
...(indexes.length >= 1 ? { item: dt.row(indexes[0]).data() } : {}),
}
self.$emit('select', payload)
self.lastSelectedItem = payload.item || null
})
this.datatable.on('deselect.dt', function (e, dt, type, indexes) {
self.$emit('deselect', {
e,
dt,
type,
indexes,
item: self.lastSelectedItem || null,
})
})
if (this.defaultSortingColumn) {
this.datatable
.column(this.defaultSortingColumn)
.order(this.defaultSortingColumnDirection || 'asc')
this.draw()
}
setTimeout(() => {
self.$emit('init', {
name: this.name,
})
if (this.autoHeight) {
this.setHeightFromParent()
}
}, initDelay)
this.initialized = true
},
onDraw() {
let rendeables =
(!!this.$refs.root &&
Array.prototype.slice.call(
this.$refs.root.querySelectorAll('[data-needs-vue-rendering]'),
0
)) ||
[]
rendeables.forEach((el) => {
let ComponentClass = Vue.extend(dynamicComponents[el.dataset.component])
let row = JSON.parse(decodeURI(atob(el.dataset.row || btoa('{}'))))
let instance = new ComponentClass({
parent: this,
propsData: {
row,
datatable: () => this.datatable,
vueDatatable: this,
},
})
instance.$mount()
let parentNode = el.parentNode
let index = Array.from(parentNode.children).indexOf(el)
parentNode.removeChild(el)
if (index == Array.from(parentNode.children).length - 1) {
parentNode.appendChild(instance.$el)
} else {
parentNode.insertBefore(
instance.$el,
Array.from(parentNode.children)[index + 1]
)
}
})
if (this.autoHeight) {
this.setHeightFromParent()
}
},
getInnerHTMLFromVueRenderable: function (htmlRenderable) {
let el = $(htmlRenderable).get(0)
let ComponentClass = Vue.extend(dynamicComponents[el.dataset.component])
let row = JSON.parse(decodeURI(atob(el.dataset.row || btoa('{}'))))
let instance = new ComponentClass({
parent: this,
propsData: {
row,
datatable: () => this.datatable,
vueDatatable: this,
},
})
instance.$mount()
return instance.$el.outerHTML
},
},
}
</script>
<style lang="scss">
.datatable th {
color: #014470 !important;
font-size: 12px;
}
.datatable td {
color: #524d4d;
font-size: 12px;
}
.dataTable__table {
width: 100% !important;
}
.dataTable_cmp__rows_select {
margin: 0px 5px;
}
.datatable_toolbar {
display: grid;
grid-template-columns: 50% 50%;
}
.datatable_filter {
justify-self: end;
}
.datatable.selectable tr:hover {
cursor: pointer !important;
}
.loader-wrapper {
position: absolute;
width: 100%;
top: 45%;
z-index: 10;
}
.loader-overlay > div:not(.loader-wrapper) {
opacity: 0.1;
}
</style>
Source