Skip to content

Commit

Permalink
Merge pull request #355 from SandroHc/multiple-classes
Browse files Browse the repository at this point in the history
Fix issues with multiple classes
  • Loading branch information
johanneswilm authored Nov 27, 2023
2 parents 9a032c2 + 6fc5a6d commit fba35f7
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 63 deletions.
5 changes: 5 additions & 0 deletions docs/documentation/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@
```

Allows for customizing the classnames used by simple-datatables.

Please note that class names cannot be empty and cannot be reused between different attributes. This is required for simple-datatables to work correctly.

Multiple classes can be provided per attribute. Please make sure that classes are be separated by spaces.
For example, `dt.options.classes.table = "first second"` will apply classes `first` and `second` to the generated table.
11 changes: 7 additions & 4 deletions src/column_filter/column_filter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {DataTable} from "../datatable"
import {createElement} from "../helpers"
import {classNamesToSelector, createElement} from "../helpers"

import {
defaultConfig
Expand Down Expand Up @@ -49,7 +49,8 @@ class ColumnFilter {
return
}

let buttonDOM : (HTMLElement | null) = this.dt.wrapperDOM.querySelector(`.${this.options.classes.button}`)
const buttonSelector = classNamesToSelector(this.options.classes.button)
let buttonDOM : (HTMLElement | null) = this.dt.wrapperDOM.querySelector(buttonSelector)
if (!buttonDOM) {
buttonDOM = createElement(
"button",
Expand All @@ -59,7 +60,8 @@ class ColumnFilter {
}
)
// filter button not part of template (could be default template. We add it to search.)
const searchWrapper = this.dt.wrapperDOM.querySelector(`.${this.dt.options.classes.search}`)
const searchSelector = classNamesToSelector(this.dt.options.classes.search)
const searchWrapper = this.dt.wrapperDOM.querySelector(searchSelector)
if (searchWrapper) {
searchWrapper.appendChild(buttonDOM)
} else {
Expand Down Expand Up @@ -170,7 +172,8 @@ class ColumnFilter {
this.wrapperDOM.style.top = `${y}px`
this.wrapperDOM.style.left = `${x}px`
} else if (this.menuDOM.contains(target)) {
const li = target.closest(`.${this.options.classes.menu} > li`) as HTMLElement
const menuSelector = classNamesToSelector(this.options.classes.menu)
const li = target.closest(`${menuSelector} > li`) as HTMLElement
if (!li) {
return
}
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const defaultConfig: DataTableConfiguration = {
template: layoutTemplate,

// Customize the class names used by datatable for different parts
classes: { // Note: use single class names
classes: {
active: "datatable-active",
ascending: "datatable-ascending",
bottom: "datatable-bottom",
Expand Down
51 changes: 27 additions & 24 deletions src/datatable.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {
cellToText,
classNamesToSelector,
containsClass,
createElement,
isObject,
joinWithSpaces,
visibleToColumnIndex
} from "./helpers"
import {
Expand Down Expand Up @@ -161,7 +164,7 @@ export class DataTable {
* Initialize the instance
*/
init() {
if (this.initialized || this.dom.classList.contains(this.options.classes.table)) {
if (this.initialized || containsClass(this.dom, this.options.classes.table)) {
return false
}

Expand Down Expand Up @@ -208,7 +211,8 @@ export class DataTable {

this.wrapperDOM.innerHTML = this.options.template(this.options, this.dom)

const selector = this.wrapperDOM.querySelector(`select.${this.options.classes.selector}`)
const selectorClassSelector = classNamesToSelector(this.options.classes.selector)
const selector = this.wrapperDOM.querySelector(`select${selectorClassSelector}`)

// Per Page Select
if (selector && this.options.paging && this.options.perPageSelect) {
Expand All @@ -225,10 +229,12 @@ export class DataTable {
selector.parentElement.removeChild(selector)
}

this.containerDOM = this.wrapperDOM.querySelector(`.${this.options.classes.container}`)
const containerSelector = classNamesToSelector(this.options.classes.container)
this.containerDOM = this.wrapperDOM.querySelector(containerSelector)

this._pagerDOMs = []
Array.from(this.wrapperDOM.querySelectorAll(`.${this.options.classes.pagination}`)).forEach(el => {
const paginationSelector = classNamesToSelector(this.options.classes.pagination)
Array.from(this.wrapperDOM.querySelectorAll(paginationSelector)).forEach(el => {
if (!(el instanceof HTMLElement)) {
return
}
Expand All @@ -245,7 +251,8 @@ export class DataTable {
}


this._label = this.wrapperDOM.querySelector(`.${this.options.classes.info}`)
const infoSelector = classNamesToSelector(this.options.classes.info)
this._label = this.wrapperDOM.querySelector(infoSelector)

// Insert in to DOM tree
this.dom.parentElement.replaceChild(this.wrapperDOM, this.dom)
Expand Down Expand Up @@ -445,7 +452,7 @@ export class DataTable {

]
}
tableVirtualDOM.attributes.class = tableVirtualDOM.attributes.class ? `${tableVirtualDOM.attributes.class} ${this.options.classes.table}` : this.options.classes.table
tableVirtualDOM.attributes.class = joinWithSpaces(tableVirtualDOM.attributes.class, this.options.classes.table)
if (this.options.tableRender) {
const renderedTableVirtualDOM : (elementNodeType | void) = this.options.tableRender(this.data, tableVirtualDOM, "header")
if (renderedTableVirtualDOM) {
Expand Down Expand Up @@ -488,7 +495,8 @@ export class DataTable {
_bindEvents() {
// Per page selector
if (this.options.perPageSelect) {
const selector = this.wrapperDOM.querySelector(`select.${this.options.classes.selector}`)
const selectorClassSelector = classNamesToSelector(this.options.classes.selector)
const selector = this.wrapperDOM.querySelector(selectorClassSelector)
if (selector && selector instanceof HTMLSelectElement) {
// Change per page
selector.addEventListener("change", () => {
Expand All @@ -505,14 +513,15 @@ export class DataTable {
// Search input
if (this.options.searchable) {
this.wrapperDOM.addEventListener("input", (event: InputEvent) => {
const inputSelector = classNamesToSelector(this.options.classes.input)
const target = event.target
if (!(target instanceof HTMLInputElement) || !target.matches(`.${this.options.classes.input}`)) {
if (!(target instanceof HTMLInputElement) || !target.matches(inputSelector)) {
return
}
event.preventDefault()

const searches: { terms: string[], columns: (number[] | undefined) }[] = []
const searchFields = Array.from(this.wrapperDOM.querySelectorAll(`.${this.options.classes.input}`)) as HTMLInputElement[]
const searchFields: HTMLInputElement[] = Array.from(this.wrapperDOM.querySelectorAll(inputSelector))
searchFields.filter(
el => el.value.length
).forEach(
Expand Down Expand Up @@ -566,16 +575,12 @@ export class DataTable {
if (hyperlink.hasAttribute("data-page")) {
this.page(parseInt(hyperlink.getAttribute("data-page"), 10))
event.preventDefault()
} else if (
hyperlink.classList.contains(this.options.classes.sorter)
) {
} else if (containsClass(hyperlink, this.options.classes.sorter)) {
const visibleIndex = Array.from(hyperlink.parentElement.parentElement.children).indexOf(hyperlink.parentElement)
const columnIndex = visibleToColumnIndex(visibleIndex, this.columns.settings)
this.columns.sort(columnIndex)
event.preventDefault()
} else if (
hyperlink.classList.contains(this.options.classes.filter)
) {
} else if (containsClass(hyperlink, this.options.classes.filter)) {
const visibleIndex = Array.from(hyperlink.parentElement.parentElement.children).indexOf(hyperlink.parentElement)
const columnIndex = visibleToColumnIndex(visibleIndex, this.columns.settings)
this.columns.filter(columnIndex)
Expand Down Expand Up @@ -675,7 +680,7 @@ export class DataTable {
this.dom.innerHTML = this._initialInnerHTML

// Remove the className
this.dom.classList.remove(this.options.classes.table)
this.options.classes.table?.split(" ").forEach(className => this.wrapperDOM.classList.remove(className))

// Remove the containers
if (this.wrapperDOM.parentElement) {
Expand All @@ -697,7 +702,7 @@ export class DataTable {
this.hasRows = Boolean(this.data.data.length)
this.hasHeadings = Boolean(this.data.headings.length)
}
this.wrapperDOM.classList.remove(this.options.classes.empty)
this.options.classes.empty?.split(" ").forEach(className => this.wrapperDOM.classList.remove(className))

this._paginate()
this._renderPage()
Expand Down Expand Up @@ -968,11 +973,9 @@ export class DataTable {
*/
refresh() {
if (this.options.searchable) {
(Array.from(this.wrapperDOM.querySelectorAll(`.${this.options.classes.input}`)) as HTMLInputElement[]).forEach(
el => {
el.value = ""
}
)
const inputSelector = classNamesToSelector(this.options.classes.input)
const inputs: HTMLInputElement[] = Array.from(this.wrapperDOM.querySelectorAll(inputSelector))
inputs.forEach(el => (el.value = ""))
this._searchQueries = []
}
this._currentPage = 1
Expand Down Expand Up @@ -1034,7 +1037,7 @@ export class DataTable {
const activeHeadings = this.data.headings.filter((heading: headerCellType, index: number) => !this.columns.settings[index]?.hidden)
const colspan = activeHeadings.length || 1

this.wrapperDOM.classList.add(this.options.classes.empty)
this.options.classes.empty?.split(" ").forEach(className => this.wrapperDOM.classList.add(className))

if (this._label) {
this._label.innerHTML = ""
Expand Down Expand Up @@ -1083,7 +1086,7 @@ export class DataTable {
this._tableFooters.forEach(footer => newVirtualDOM.childNodes.push(footer))
this._tableCaptions.forEach(caption => newVirtualDOM.childNodes.push(caption))

newVirtualDOM.attributes.class = newVirtualDOM.attributes.class ? `${newVirtualDOM.attributes.class} ${this.options.classes.table}` : this.options.classes.table
newVirtualDOM.attributes.class = joinWithSpaces(newVirtualDOM.attributes.class, this.options.classes.table)

if (this.options.tableRender) {
const renderedTableVirtualDOM : (elementNodeType | void) = this.options.tableRender(this.data, newVirtualDOM, "message")
Expand Down
31 changes: 19 additions & 12 deletions src/editing/editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
classNamesToSelector,
cellToText,
columnToVisibleIndex,
createElement,
Expand Down Expand Up @@ -80,7 +81,7 @@ export class Editor {
if (this.initialized) {
return
}
this.dt.wrapperDOM.classList.add(this.options.classes.editable)
this.options.classes.editable?.split(" ").forEach(className => this.dt.wrapperDOM.classList.add(className))
if (this.options.inline) {
this.originalRowRender = this.dt.options.rowRender
this.dt.options.rowRender = (row, tr, index) => {
Expand Down Expand Up @@ -213,9 +214,10 @@ export class Editor {
return
}
if (this.editing && this.data && this.editingCell) {
const inputSelector = classNamesToSelector(this.options.classes.input)
const input = this.modalDOM ?
(this.modalDOM.querySelector(`input.${this.options.classes.input}[type=text]`) as HTMLInputElement) :
(this.dt.wrapperDOM.querySelector(`input.${this.options.classes.input}[type=text]`) as HTMLInputElement)
(this.modalDOM.querySelector(`input${inputSelector}[type=text]`) as HTMLInputElement) :
(this.dt.wrapperDOM.querySelector(`input${inputSelector}[type=text]`) as HTMLInputElement)
this.saveCell(input.value)
} else if (!this.editing) {
const cell = target.closest("tbody td") as HTMLTableCellElement
Expand All @@ -232,6 +234,7 @@ export class Editor {
* @return {Void}
*/
keydown(event: KeyboardEvent) {
const inputSelector = classNamesToSelector(this.options.classes.input)
if (this.modalDOM) {
if (event.key === "Escape") { // close button
if (this.options.cancelModal(this)) {
Expand All @@ -240,21 +243,21 @@ export class Editor {
} else if (event.key === "Enter") { // save button
// Save
if (this.editingCell) {
const input = (this.modalDOM.querySelector(`input.${this.options.classes.input}[type=text]`) as HTMLInputElement)
const input = (this.modalDOM.querySelector(`input${inputSelector}[type=text]`) as HTMLInputElement)
this.saveCell(input.value)
} else {
const values = (Array.from(this.modalDOM.querySelectorAll(`input.${this.options.classes.input}[type=text]`)) as HTMLInputElement[]).map(input => input.value.trim())
const values = (Array.from(this.modalDOM.querySelectorAll(`input${inputSelector}[type=text]`)) as HTMLInputElement[]).map(input => input.value.trim())
this.saveRow(values, this.data.row)
}
}
} else if (this.editing && this.data) {
if (event.key === "Enter") {
// Enter key saves
if (this.editingCell) {
const input = (this.dt.wrapperDOM.querySelector(`input.${this.options.classes.input}[type=text]`) as HTMLInputElement)
const input = (this.dt.wrapperDOM.querySelector(`input${inputSelector}[type=text]`) as HTMLInputElement)
this.saveCell(input.value)
} else if (this.editingRow) {
const values = (Array.from(this.dt.wrapperDOM.querySelectorAll(`input.${this.options.classes.input}[type=text]`)) as HTMLInputElement[]).map(input => input.value.trim())
const values = (Array.from(this.dt.wrapperDOM.querySelectorAll(`input${inputSelector}[type=text]`)) as HTMLInputElement[]).map(input => input.value.trim())
this.saveRow(values, this.data.row)
}
} else if (event.key === "Escape") {
Expand Down Expand Up @@ -329,7 +332,8 @@ export class Editor {
})
this.modalDOM = modalDOM
this.openModal()
const input = (modalDOM.querySelector(`input.${this.options.classes.input}[type=text]`) as HTMLInputElement)
const inputSelector = classNamesToSelector(this.options.classes.input)
const input = (modalDOM.querySelector(`input${inputSelector}[type=text]`) as HTMLInputElement)
input.focus()
input.selectionStart = input.selectionEnd = input.value.length
// Close / save
Expand Down Expand Up @@ -477,7 +481,8 @@ export class Editor {
this.modalDOM = modalDOM
this.openModal()
// Grab the inputs
const inputs = Array.from(form.querySelectorAll(`input.${this.options.classes.input}[type=text]`)) as HTMLInputElement[]
const inputSelector = classNamesToSelector(this.options.classes.input)
const inputs = Array.from(form.querySelectorAll(`input${inputSelector}[type=text]`)) as HTMLInputElement[]

// Close / save
modalDOM.addEventListener("click", (event: MouseEvent) => {
Expand Down Expand Up @@ -620,7 +625,8 @@ export class Editor {
}
let valid = true
if (this.editing) {
valid = !(target.matches(`input.${this.options.classes.input}[type=text]`))
const inputSelector = classNamesToSelector(this.options.classes.input)
valid = !(target.matches(`input${inputSelector}[type=text]`))
}
if (valid) {
this.closeMenu()
Expand All @@ -633,9 +639,10 @@ export class Editor {
*/
openMenu() {
if (this.editing && this.data && this.editingCell) {
const inputSelector = classNamesToSelector(this.options.classes.input)
const input = this.modalDOM ?
(this.modalDOM.querySelector(`input.${this.options.classes.input}[type=text]`) as HTMLInputElement) :
(this.dt.wrapperDOM.querySelector(`input.${this.options.classes.input}[type=text]`) as HTMLInputElement)
(this.modalDOM.querySelector(`input${inputSelector}[type=text]`) as HTMLInputElement) :
(this.dt.wrapperDOM.querySelector(`input${inputSelector}[type=text]`) as HTMLInputElement)

this.saveCell(input.value)
}
Expand Down
49 changes: 49 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,52 @@ export const namedNodeMapToObject = function(map: NamedNodeMap) {
}
return obj
}

/**
* Convert class names to a CSS selector. Multiple classes should be separated by spaces.
* Examples:
* - "my-class" -> ".my-class"
* - "my-class second-class" -> ".my-class.second-class"
*
* @param classNames The class names to convert. Can contain multiple classes separated by spaces.
*/
export const classNamesToSelector = (classNames: string) => {
if (!classNames) {
return null
}
return classNames.trim().split(" ").map(className => `.${className}`).join("")
}

/**
* Check if the element contains all the classes. Multiple classes should be separated by spaces.
*
* @param element The element that will be checked
* @param classes The classes that must be present in the element. Can contain multiple classes separated by spaces.
*/
export const containsClass = (element: Element, classes: string) => {
const hasMissingClass = classes?.split(" ").some(className => !element.classList.contains(className))
return !hasMissingClass
}

/**
* Join two strings with spaces. Null values are ignored.
* Examples:
* - joinWithSpaces("a", "b") -> "a b"
* - joinWithSpaces("a", null) -> "a"
* - joinWithSpaces(null, "b") -> "b"
* - joinWithSpaces("a", "b c") -> "a b c"
*
* @param first The first string to join
* @param second The second string to join
*/
export const joinWithSpaces = (first: string | null | undefined, second: string | null | undefined) => {
if (first) {
if (second) {
return `${first} ${second}`
}
return first
} else if (second) {
return second
}
return ""
}
6 changes: 4 additions & 2 deletions src/rows.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {readDataCell} from "./read_data"
import {DataTable} from "./datatable"
import {cellType, dataRowType, inputCellType} from "./types"
import {cellToText} from "./helpers"
import {cellToText, classNamesToSelector} from "./helpers"

/**
* Rows API
*/
Expand All @@ -24,7 +25,8 @@ export class Rows {
this.cursor = index
this.dt._renderTable()
if (index !== false && this.dt.options.scrollY) {
const cursorDOM = this.dt.dom.querySelector(`tr.${this.dt.options.classes.cursor}`)
const cursorSelector = classNamesToSelector(this.dt.options.classes.cursor)
const cursorDOM = this.dt.dom.querySelector(`tr${cursorSelector}`)
if (cursorDOM) {
cursorDOM.scrollIntoView({block: "nearest"})
}
Expand Down
Loading

0 comments on commit fba35f7

Please sign in to comment.