org.magicwerk.brownies.html.content.htmlTableFormatter.ts Maven / Gradle / Ivy
The newest version!
// HtmlTableFormatter
// Classes
class ColAttr {
col: JqHtmlElem
colIndex: number
colName: string
colType: string
sortPos: number = null
/** null: no sorting, false: ascending, true: descending */
sortOrder: boolean = null
filterStr: string = null
filter: ColFilter = null
constructor(col: any, colIndex: number) {
this.col = col
this.colIndex = colIndex
this.colType
this.sortPos
this.sortOrder
this.filterStr
this.filter
}
clearSort() {
this.sortPos = null
this.sortOrder = null
}
clearFilter() {
this.filterStr = null
this.filter = null
}
}
// Simple click:
// - on column without sorting: clear existing sorting, apply sorting to new column
// - on column with sorting: modify existing sorting of column (also if part of multi sort)
// Click with shift/ctrl:
// - on column without sorting: apply sorting to new column
// - on column with sorting: as above
class TableAttr {
table: JqHtmlElem
cols: ColAttr[] = []
numSort: number
constructor(table: any) {
this.table = table
}
clearSort() {
for (let i=0; i colAttr.sortPos) {
this.cols[i].sortPos--
}
}
colAttr.sortOrder = null
colAttr.sortPos = null
this.numSort--
}
}
}
}
// Functions
$(document).ready(function() {
activateTables()
})
function activateTables(node?: JqHtmlElem) {
if (!node) {
node = $(document.documentElement)
}
$(node).find(".activeTable").each(function() {
activateTable($(this))
})
}
function activateTable(table: JqHtmlElem) {
let ta = new TableAttr(table)
table.find('tr:nth-child(1) th').each(function(index) {
let ca = new ColAttr($(this), index)
ta.cols.push(ca)
activateCol(ta, ca)
});
table.find('tr:not(:first)').each(function(index) {
setDataRowNum($(this), index)
});
}
function setDataRowNum(tr: JqHtmlElem, index: number) {
tr.data('rownum', index)
}
function getDataRowNum(tr: JqHtmlElem) {
return Sort.toNumber(tr.data('rownum'))
}
function activateCol(tabAttr: TableAttr, colAttr: ColAttr) {
colAttr.colName = getHeaderElem(tabAttr.table, colAttr.colIndex).text()
colAttr.colType = getColType(tabAttr, colAttr)
log2("activateCol " + colAttr.colIndex + " -> " + colAttr.colType)
// Attach handlers
colAttr.col.click(function(e) {
manageSort(tabAttr, colAttr, e.shiftKey || e.ctrlKey)
})
colAttr.col.on('contextmenu', e => {
manageFilter(tabAttr, colAttr)
e.preventDefault()
})
}
function getColType(tabAttr: TableAttr, colAttr: ColAttr) {
let vals = getColValues(tabAttr.table, colAttr.colIndex)
let type = null
for (let i=0; i 0
case Operation.GE: return cmp >= 0
case Operation.LT: return cmp < 0
case Operation.LE: return cmp <= 0
case Operation.RQ: return cmp === 0
case Operation.NR: return cmp !== 0
default: assert2(false)
}
}
/** Returns operation for specified string, null if not found */
static getOperation(str: string): Operation {
switch (str) {
case "=": return Operation.EQ
case "!=": return Operation.NE
case ">": return Operation.GT
case ">=": return Operation.GE
case "<": return Operation.LT
case "<=": return Operation.LE
case "~": return Operation.RQ
case "!~": return Operation.NR
default: assert2(false)
}
}
static isRegexOperation(op: Operation) {
return op === Operation.RQ || op === Operation.NR
}
}
// Filter column
/** Show dialog to manage filter. */
function manageFilter(tabAttr: TableAttr, colAttr: ColAttr) {
let helpText = `
"String / Number
""
Example:
"abc": value must be equal to 'abc'
Regex:
"~": match regex
"!~": regex does not match
Example:
"~ action": value must match regex 'action'
String / Number:
"=", "!=", ">", ">=", "<", "<="
Example:
"> 10": values must be >10"
`
let okFnc = function(text: string) {
text = text.trim()
colAttr.filterStr = (text !== '') ? text : null
if (colAttr.filterStr) {
colAttr.filter = createFilter(colAttr, colAttr.filterStr)
} else {
colAttr.filter = null
}
removeFilter(colAttr)
addFilter(colAttr)
let selected = applyFilter(tabAttr)
// Show information about selected rows
let all = getNumRows(tabAttr.table)
let msg = "Selected " + selected + " of " + all + " rows"
showInfo(msg)
}
let title = "Filter " + colAttr.colName
let value = (colAttr.filterStr) ? colAttr.filterStr : ""
showDialog({ title: title, value: value, help: helpText, ok: okFnc })
}
/** Show information in a non-modal dialog which will automatically close */
function showInfo(text: string) {
// As there still maybe a dialog open, show the info dialog using setTimeout
setTimeout(() => { showDialog({ label: text, autoClose: 1000 }) }, 0 )
}
/** Return created filter */
function createFilter(colAttr: ColAttr, str: string): ColFilter {
let op: Operation = null
let argStr: string = null
let match = str.match(/^([~!<>=]+)\s*(.*)$/)
if (match) {
op = ColFilter.getOperation(match[1])
if (op) {
if (ColFilter.isRegexOperation(op)) {
if (colAttr.colType !== TYPE_STRING) {
op = null
}
}
}
if (op) {
argStr = match[2]
}
}
if (op == null) {
op = Operation.EQ
argStr = str
}
let arg = getValue(colAttr.colType, argStr)
return new ColFilter(colAttr.colType, op, arg)
}
function getValue(type: string, valStr: string): any {
if (type === TYPE_NUMBER) {
return Sort.toNumber(valStr)
} else {
return valStr
}
}
/** Apply filter and return number of selected rows */
function applyFilter(tabAttr: TableAttr): number {
let rs = getNumRows(tabAttr.table)
let selected = 0
for (let r=0; r 0
}
function addFilter(colAttr: ColAttr) {
if (colAttr.filterStr) {
let elem = $("").text(colAttr.filterStr)
colAttr.col.append(elem)
}
}
function removeFilter(colAttr: ColAttr) {
colAttr.col.find('.filter').remove()
}
// Sort column
function manageSort(tabAttr: TableAttr, colAttr: ColAttr, modifier: boolean) {
log2("manageSort " + colAttr.colIndex + " " + modifier)
tabAttr.changeSort(colAttr, modifier)
renderSort(tabAttr)
applySort(tabAttr)
}
function renderSort(tabAttr: TableAttr) {
for (let i=0; i number)[] = []
add(cmp: (o1: any, o2: any) => number) {
this.comparators.push(cmp)
}
get() {
let self = this
return function(o1, o2) {
for (let i=0; i 1) {
text += getNumberText(colAttr.sortPos)
}
let elem = $("").text(text)
// Add sort indicator before filter if there is any
// TODO: it would probably be easier to always have .sort and .filter and just make the elements invisible
let filterElem = colAttr.col.find('.filter')
if (filterElem.length > 0) {
filterElem.before(elem)
} else {
colAttr.col.append(elem)
}
}
function getArrowText(order: boolean) {
if (order === false) {
return '\u25b2' // arrow up
} else if (order === true) {
return '\u25bc' // arrow up
} else {
assert2(false)
}
}
function getNumberText(pos: number) {
let number1 = 0x2780
return String.fromCharCode(number1 + pos);
}
function removeSort(colAttr: ColAttr) {
colAttr.col.find('.sort').remove()
}
// Method for HTML tables
function getHeaderElem(table: JqHtmlElem, col: number) {
let c = col + 1 // index has base 1
let sel = 'tr:nth-child(1) th:nth-child('+ c +')'
return table.find(sel)
}
function getColNames(table: JqHtmlElem) {
let names = []
table.find('tr:nth-child(1) th').each( function(){
names.push( $(this).text() );
});
return names
}
function getNumCols(table: JqHtmlElem) {
let numCols = table.find('tr:nth-child(1) th').length
return numCols
}
function getNumRows(table: JqHtmlElem) {
let numRows = table.find('tr').length
return numRows - 1 // subtract 1 for header row
}
function getTableValues(table: JqHtmlElem) {
let rows = []
let rs = getNumRows(table)
let cs = getNumCols(table)
for (let r=0; r(array: T[], f: (e1: T, e2: T) => number, undefFirst: boolean = false): T[] {
// Note that array.sort always sorts undefined elements at the end without calling the passed comparsion function.
array.sort(f)
if (undefFirst) {
let index = array.indexOf(undefined)
if (index !== -1) {
let array2 = array.splice(index, array.length-index).concat(array.splice(0, index))
Sort.assign(array, array2)
}
}
return array
}
/**
* Create function which compares two array by their value at the specified position.
* The two values retrieved are compared using objects.compareNatural.
* The created function receives the two arrays as input and can be used by calling arrays.sort.
*/
static createArrayComparator(index: IInteger, desc: boolean = false, nullsFirst: boolean = false) {
let f = function(arr1: any[], arr2: any[]): number {
let v1 = arr1[index]
let v2 = arr2[index]
return Sort.compareNatural(v1, v2, desc, nullsFirst)
}
return f
}
static compareNatural(any1: any, any2: any, desc: boolean = false, nullsFirst: boolean = true): IInteger {
if (Sort.isValue(any1) && Sort.isValue(any2)) {
// Compare two valid values
if (Sort.isNumber(any1) && Sort.isNumber(any2)) {
return Sort.compareNumbers(any1, any2, desc, nullsFirst)
} else if (Sort.isString(any1) && Sort.isString(any2)) {
return Sort.compareStrings(any1, any2, desc, nullsFirst)
} else {
let str1 = Sort.toString(any1)
let str2 = Sort.toString(any2)
return Sort.compareStrings(str1, str2, desc, nullsFirst)
}
} else {
// At least one value is null or undefined
return Sort.doCompareNull(any1, any2, desc, nullsFirst)
}
}
static doCompareNull(any1: any, any2: any, desc: boolean = false, nullsFirst: boolean = true) {
// At least one value is null or undefined
let v1 = (any1 === null) ? 1 : ((any1 === undefined) ? 2 : 0)
let v2 = (any2 === null) ? 1 : ((any2 === undefined) ? 2 : 0)
if ( nullsFirst ) {
// map 1,2 to to -1,-2
v1 = (v1 >= 1) ? -v1 : v1
v2 = (v2 >= 1) ? -v2 : v2
}
let n = v1 - v2
if ( desc ) {
if (Math.abs(v1) < 1 && Math.abs(v2) < 1) {
n = -n
}
}
return n
}
static compareNumbers(num1: number, num2: number, desc: boolean = false, nullsFirst: boolean = false): IInteger {
let n
let f1 = Sort.isValue(num1) && isFinite(num1)
let f2 = Sort.isValue(num2) && isFinite(num2)
if (f1 && f2) {
n = num1 - num2
if ( desc ) {
n = -n
}
//log("compareNumbers: {0} vs {1} -> {2}", num1, num2, n)
} else {
// Order -Infinity, ALL_FINITE_NUMBERS, +Infinity, NaN, null, undefined
let n1 = f1 ? 0 : (Number.isNaN(num1) ? 2 : ((num1 === -Infinity) ? -1 : ((num1 === Infinity) ? 1: ((num1 === null) ? 3 : 4))))
let n2 = f2 ? 0 : (Number.isNaN(num2) ? 2 : ((num2 === -Infinity) ? -1 : ((num2 === Infinity) ? 1: ((num2 === null) ? 3 : 4))))
if ( nullsFirst ) {
// map 2,3,4 to -4,-3,-2
n1 = (n1 >= 2) ? -n1 : n1
n2 = (n2 >= 2) ? -n2 : n2
}
n = n1 - n2
if ( desc ) {
if (Math.abs(n1) < 2 && Math.abs(n2) < 2) {
n = -n
}
}
//log("compareNumbers: {0} vs {1} -> {2} ({3} vs {4})", num1, num2, n, n1, n2)
}
return n
}
static compareStrings(str1: string, str2: string, desc: boolean = false, nullsFirst: boolean = false): IInteger {
let n
let isVal1 = Sort.isValue( str1 )
let isVal2 = Sort.isValue( str2 )
if (isVal1 && isVal2 ) {
let options = undefined
// The locale typically already sorts ignoring the case, i.e. a1, A2, a3
//if (caseInsensitive) {
// options = { sensitivity: 'base' }
//}
n = str1.localeCompare( str2, undefined, options )
if ( desc ) {
n = -n
}
} else {
n = Sort.doCompareNull(str1, str2, desc, nullsFirst)
}
return n
}
static assign( dst: T[], src: T[] ) {
dst.length = 0
for ( let i = 0; i < src.length; i++ ) {
dst.push( src[i] )
}
}
static isValue( obj: any ) {
return !(obj === null || obj === undefined)
}
static isNumber( obj: any ) {
return Object.prototype.toString.call( obj ) === "[object Number]"
}
static isString( obj: any ) {
return Object.prototype.toString.call( obj ) === "[object String]"
}
/**
* Returns input value converted to a number.
* If the input is null or undefined, the input value is returned unchanged.
* If the input value cannot be converted to a number, NaN is returned.
*/
static toNumber( val: any ): number {
if ( !this.isValue( val ) ) {
return val
}
return Number( val )
}
static toString( val: any ): string {
if ( !this.isValue( val ) ) {
return val
}
return String( val )
}
static isEmpty( str: string ): boolean {
if ( !this.isValue( str ) ) {
return true
}
return str.length === 0
}
}
function assert2(cond: boolean) {
if (!cond) {
throw new Error("Assertion failed")
}
}
function log2(msg: any) {
console.log(msg);
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy