Skip to content

Instantly share code, notes, and snippets.

@mskoroglu
Last active April 20, 2022 08:26
Show Gist options
  • Save mskoroglu/4080bd06910c2c45e6c8625c5174c6a4 to your computer and use it in GitHub Desktop.
Save mskoroglu/4080bd06910c2c45e6c8625c5174c6a4 to your computer and use it in GitHub Desktop.
jQuery DataTables with Kotlin
/**
* @author Mustafa Köroğlu
*/
const val kTableFunction = """
function kTable(table, configURL, cb) {
$.post(configURL, function (config) {
config.options.ajax.data = JSON.stringify;
config.column = function (columnName) {
return config.options.columns.filter(function (row) {
return row.name === columnName;
})[0];
};
config.render = function (columnName, cb) {
config.column(columnName).render = function (data, type, row, meta) {
var value = data;
var cell = row.cells[meta.col];
var cellData = cell.dataAttributes;
var rowData = row.dataAttributes;
return cb(value, cellData, cell, rowData, row);
};
};
var extendedConfig = cb ? cb(config) : config.options;
var customCreatedRow = extendedConfig.createdRow;
extendedConfig.createdRow = function (td, cellData, rowData, row, col) {
if (cellData.dataAttributes) Object.keys(cellData.dataAttributes).forEach(function (key) {
$(td).attr('data-' + key, cellData.dataAttributes[key]);
});
cellData.cells.forEach(function (cell, i) {
if (cell.dataAttributes) Object.keys(cell.dataAttributes).forEach(function (key) {
$(row[i]).attr('data-' + key, cell.dataAttributes[key]);
});
});
if (customCreatedRow) customCreatedRow.apply(this, arguments);
};
table.DataTable(extendedConfig);
});
}
"""
fun <T> dataTable(url: String, ignoreCase: Boolean = true, regex: Boolean = false, init: DataTable<T>.() -> Unit): DataTable<T> {
val table = DataTable<T>(url = url, ignoreCase = ignoreCase, regex = regex)
table.init()
return table
}
class DataTable<T> internal constructor(private val url: String, private val ignoreCase: Boolean = true, private val regex: Boolean = false) {
private val columnBuilders: MutableSet<ColumnBuilder<T>> = mutableSetOf()
private val dataAttributes: MutableMap<String, T.() -> Any?> = mutableMapOf()
val config: MutableMap<String, Any>
get() = mutableMapOf("options" to mutableMapOf(
"ajax" to mutableMapOf(
"url" to url,
"method" to "POST",
"contentType" to "application/json"
),
"serverSide" to true,
"processing" to true,
"searchDelay" to 500,
"columns" to columnBuilders.mapIndexed { i, it ->
mutableMapOf(
"data" to "cells.$i.value",
"name" to it.name,
"searchable" to it.isSearchable,
"orderable" to it.isSortable
)
},
"order" to arrayOf(arrayOf(columnBuilders.firstOrNull { it.isSortable }?.index, "asc"))
))
fun create(query: Query, collection: Collection<T>): Result {
val recordsTotal = collection.size.toLong()
var rowSequence: Sequence<Row<T>>? = null
// SEARCHING
val searchKey = query.search.value
if (searchKey.isNotBlank()) {
val filteredRows = collection.asSequence().map { item ->
val cells = columnBuilders.filter { it.isSearchable }.map { builder ->
Cell(builder).apply {
searchValue = if (builder.ref === builder.searchRef) {
value = builder.ref(item)
value
} else builder.searchRef(item)
}
}
val searchMatched = cells.map { it.searchValue }.any {
if (!regex) it.toString().contains(searchKey, ignoreCase) else try {
val options = mutableSetOf<RegexOption>()
if (ignoreCase) options.add(RegexOption.IGNORE_CASE)
Regex(searchKey, options).matches(it.toString())
} catch (e: Exception) {
false
}
}
return@map if (!searchMatched) null else Row(item = item, cells = cells.toMutableList())
}
rowSequence = filteredRows.filter { it != null }.map { it!! }
}
// SORTING
val sort = query.order.first()
val sortBuilder = columnBuilders.toList()[sort.column]
if (sortBuilder.isSortable) {
rowSequence = (rowSequence ?: collection.asSequence().map { Row(item = it) }).map { row ->
val builder = columnBuilders.find { it === sortBuilder }!!
val cellByKey = row.cells.find { it.key == builder.name }
val cell = cellByKey ?: Cell(builder)
if (!cell.sortValueInvoked) {
cell.sortValue = if (builder.ref === builder.sortRef) {
if (!cell.valueInvoked)
cell.value = builder.ref(row.item)
cell.value
} else builder.sortRef(row.item)
}
if (cellByKey == null) row.cells.add(cell)
return@map row
}
val selector: (Row<T>) -> Comparable<Any?> = {
@Suppress("UNCHECKED_CAST")
it.cells.find { cell -> cell.key == sortBuilder.name }!!.sortValue as Comparable<Any?>
}
rowSequence = if (sort.dir == "asc") rowSequence.sortedBy(selector)
else rowSequence.sortedByDescending(selector)
}
// PAGING
val filteredAndSorted = (rowSequence ?: collection.asSequence().map { Row(item = it) }).toList()
val fromIndex = query.start
val to = fromIndex + query.length
val toIndex = if (to > filteredAndSorted.size) filteredAndSorted.size else to
val page = filteredAndSorted.subList(fromIndex, toIndex)
// MAPPING
val mappedResult = page.map { row ->
val item = row.item
columnBuilders.forEach { builder ->
val cellByKey = row.cells.find { it.key == builder.name }
val cell = cellByKey ?: Cell(builder)
if (!cell.valueInvoked) cell.value = builder.ref(item)
builder.dataAttributes.forEach { cell.dataAttributes[it.key] = it.value(item) }
if (cellByKey == null) row.cells.add(cell)
}
this.dataAttributes.forEach { row.dataAttributes[it.key] = it.value(item) }
val rowMap = mutableMapOf<String, Any?>(
"cells" to row.cells.sortedBy { it.builder.index }.map { cell ->
val cellMap = mutableMapOf("value" to cell.value)
if (cell.dataAttributes.isNotEmpty())
cellMap["dataAttributes"] = cell.dataAttributes
cellMap
}
)
if (row.dataAttributes.isNotEmpty()) rowMap["dataAttributes"] = row.dataAttributes
return@map rowMap
}
return Result(
draw = query.draw,
recordsTotal = recordsTotal,
recordsFiltered = filteredAndSorted.size.toLong(),
data = mappedResult
)
}
fun column(name: String, ref: T.() -> Any?) = ColumnBuilder(columnBuilders.size, name, ref).also {
columnBuilders.add(it)
}
fun computed(name: String, ref: T.() -> Any?) {
columnBuilders.add(ColumnBuilder(columnBuilders.size, name, ref))
}
fun rowData(init: Data<T>.() -> Unit) {
val data = Data<T>()
data.init()
this.dataAttributes.putAll(data.map)
}
class Data<T>(internal val map: MutableMap<String, T.() -> Any?> = mutableMapOf()) {
infix fun String.to(other: T.() -> Any?) {
map[this] = other
}
}
class ColumnBuilder<T>(internal var index: Int, internal val name: String, internal val ref: T.() -> Any?) {
internal var isSortable: Boolean = false
internal var isSearchable: Boolean = false
internal var sortRef: T.() -> Any? = ref
internal var searchRef: T.() -> Any? = ref
internal val dataAttributes: MutableMap<String, T.() -> Any?> = mutableMapOf()
internal fun sort() = this.apply { isSortable = true }
internal fun sort(ref: T.() -> Any?) = this.apply { isSortable = true; sortRef = ref }
internal fun search() = this.apply { isSearchable = true }
internal fun search(ref: T.() -> Any?) = this.apply { isSearchable = true; searchRef = ref }
fun data(init: Data<T>.() -> Unit): ColumnBuilder<T> {
val data = Data<T>()
data.init()
this.dataAttributes.putAll(data.map)
return this
}
}
private class Row<T>(val item: T, val cells: MutableList<Cell<T>> = mutableListOf(), val dataAttributes: MutableMap<String, Any?> = mutableMapOf())
private class Cell<T>(val builder: ColumnBuilder<T>) {
val key: String = builder.name
var valueInvoked = false
var value: Any? = null
set(value) {
field = value
valueInvoked = true
}
var searchValue: Any? = null
var sortValueInvoked = false
var sortValue: Any? = null
set(value) {
field = value
sortValueInvoked = true
}
val dataAttributes: MutableMap<String, Any?> = mutableMapOf()
}
}
class Query(val draw: Int, val order: List<Order> = emptyList(), val start: Int, val length: Int, val search: Search) {
class Order(var column: Int, var dir: String)
class Search(var value: String)
}
class Result(val draw: Int, val recordsTotal: Long, val recordsFiltered: Long, val data: Collection<*>, val error: String? = null)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment