Skip to content

Commit

Permalink
Fix issues with multiple classes
Browse files Browse the repository at this point in the history
Fixes all locations where multiple classes were not working as expected.

Example that was failing before:
```html
<table></table>
<script>
    new DataTable("table", {
        classes: {
            container: "my-container second-class",
        }
    })
</script>
```
  • Loading branch information
SandroHc committed Nov 27, 2023
1 parent 9a032c2 commit 6fc5a6d
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 6fc5a6d

Please sign in to comment.