Skip to content

Instantly share code, notes, and snippets.

@a-c-t-i-n-i-u-m
Created February 12, 2016 15:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a-c-t-i-n-i-u-m/79ad0fa1b0a20f1e13bb to your computer and use it in GitHub Desktop.
Save a-c-t-i-n-i-u-m/79ad0fa1b0a20f1e13bb to your computer and use it in GitHub Desktop.
(function () {
'use strict';
/*
* Utils
*/
// iteration
// https://gist.github.com/a-c-t-i-n-i-u-m/8a331f807d0329c66c5d
var each = function (obj, callback, thisArg) {
if (!obj || typeof callback !== 'function') return;
thisArg = arguments.length === 3 ? thisArg : obj;
if (!isNaN(obj.length)) {
var len = +obj.length;
for (var i = 0; i < len; i++) {
if (callback.call(thisArg, obj[i], i, obj) === false) break;
}
} else {
for (var k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
if (callback.call(thisArg, obj[k], k, obj) === false) break;
}
}
}
return thisArg;
};
// attr
var attr = function (el, key, val) {
if (!el || !el.getAttribute || !key) {
return;
}
// when get
if (arguments.length === 2 && typeof key === 'string') {
return el.getAttribute(key);
}
// when set/remove
var set = {};
// when set/remove; as object
if (arguments.length === 2) {
set = key;
} else if (arguments.length === 3) {
set[key] = val;
}
var dataUpdate = false;
each(set, function (v, k) {
if (k.indexOf('data') === 0) {
dataUpdate = true;
}
if (v === null) {
el.removeAttribute(k);
} else {
el.setAttribute(k, v);
}
});
if (dataUpdate) {
el.className = el.className;// force update page
}
};
// event
var setEvent = function (el, evt, handler, capture) {
if (!el || !evt || typeof handler !== 'function') {
return;
}
capture = !!capture;
each((evt + '').split(/\s+/), function (e) {
var k = 'on' + e;
if (el.addEventListener) {
el.addEventListener(e, handler, capture);
} else if (el.attachEvent) {
el.attachEvent(k, handler);
} else {
el[k] = handler;
}
});
};
// style
var getStyle = function (el, prop, castNumber) {
var v = null;
if (window.getComputedStyle) {
v = getComputedStyle(el).getPropertyValue(prop);
} else if (el.currentStyle) {
v = el.currentStyle[prop];
} else if (el.style[prop]) {
v = el.style[prop];
} else if (el[prop]) {
v = el[prop];
} else {
return null;
}
return castNumber ? +(v + '').replace(/[^\d\.]/g, '') : v;
};
// textcontent
var getTextContent = function (el) {
return !el ? '' : ('textContent' in el ? el.textContent : ('innerText' in el ? el.innerText : el.innerHTML));
};
// position
var getAbsolutePosition = function (el) {
// actually, total offsetLeft/Top
var position = {
x: el.offsetLeft,
y: el.offsetTop
};
while (el.offsetParent) {
position.x += el.offsetParent.offsetLeft;
position.y += el.offsetParent.offsetTop;
el = el.offsetParent;
}
return position;
};
var getCursorPosition = function (evt) {
return {
x: 'pageX' in evt ? evt.pageX : evt.clientX + document.documentElement.scrollLeft,
y: 'pageY' in evt ? evt.pageY : evt.clientY + document.documentElement.scrollTop
};
};
/*
* main interface
* table: HTML table element
* options: (optional) feature selection; properties(boolean)
* sortable: sortable by clicking headers
* resizable: enable row/column resize
* columnResizable
* rowResizable
* editable: implement cell content editor
*/
var customtable = function (table, options) {
if (!table || table.nodeName !== 'TABLE') {
throw new Error('this is not a table element.');
}
this.table = table;
this.options = options || {};
var classes = ['customtable'];
for (var k in options) {
if (options[k]) {
customtable[k].call(this);
classes.push(k);
}
}
table.className += ' ' + classes.join(' ');
};
//
// sortable
//
customtable.sortable = function (table, options) {
var self = this;
self.sortState = [];
setEvent(self.table, 'mouseup', function (e) {
var el = e.target || e.srcElement;
if (
!el || el.nodeName !== 'TH'
|| attr(self.table, 'data-column-resizing')
|| attr(self.table, 'data-row-resizing')
) {
return;
}
// get sort column
var headers = el.parentElement.children,
index = -1;
each(headers, function (th, i) {
if (th === el) {
index = i;
}
});
if (index === -1) {
return;
}
// update sortState
var dir = (attr(el, 'data-sort-direction') || 'desc') === 'desc' ? 1 : -1;
if (e.shiftKey) {
var sortOrder = each(self.sortState, function (v) {
if (v[0] !== index) {
this.push(v);
}
}, []);
sortOrder.push([index, dir]);
} else {
var sortOrder = [[index, dir]];
}
// apply
self.sort(sortOrder);
});
};
// sorting process
customtable.prototype.sort = function (sortOrder) {
var self = this;
if (sortOrder && sortOrder instanceof Array) {
self.sortState = sortOrder;
}
// update header
each(self.table.tHead.rows, function (tr) {
each(tr.cells, function (th, i) {
var p = null;
each(self.sortState, function (s) {
if (s[0] === i) {
p = s[1] === 1 ? 'asc' : 'desc';
return false;// break
}
});
attr(th, 'data-sort-direction', p);
});
});
// clear table rows
var rows = [],
tbody = self.table.tBodies[0];
while (tbody.firstChild) {
rows.push(tbody.removeChild(tbody.firstChild));
}
// sorting
if (self.sortState.length > 0) {
rows.sort(function (a, b) {
var sortState = self.sortState,
options = self.options.sortable,
methods = self.sortImplementation;
for (var i = 0; i < sortState.length; i++) {
// detect sort method
var columnIndex = sortState[i][0],
method = methods.dictionary;
// specific column
if (options && options[columnIndex] && methods[options[columnIndex]]) {
method = methods[options[columnIndex]];
}
// method list
else if (options && methods[options]) {
method = methods[options];
}
// object method specify
else if (options && 'defaults' in options && methods[options.defaults]) {
method = methods[options.defaults];
}
// get value
var va = a.children && a.children[columnIndex] ? getTextContent(a.children[columnIndex]) : '',
vb = b.children && b.children[columnIndex] ? getTextContent(b.children[columnIndex]) : '';
// compare
var ret = method(va, vb) * sortState[i][1];
if (ret !== 0) {
// compare next column
return ret;
}
}
return 1;// return 0;// constant sort
});
}
// insert sorted rows
each(rows, function (row) {
tbody.appendChild(row);
});
// end
return this;
};
// sort implementation
customtable.prototype.sortImplementation = {};
customtable.prototype.sortImplementation.dictionary = function (va, vb) {
return ((va > vb) - (va < vb));
};
customtable.prototype.sortImplementation.number = function (va, vb) {
return va - vb;
};
//
// col resizable
//
customtable.columnResizable = function () {
var self = this,
table = self.table,
options = self.options.columnResizable,
threshold = isNaN(options.threshold) ? 3 : +options.threshold,
minWidth = isNaN(options.minWidth) ? 10 : +options.minWidth,
targetCell = null;
setEvent(table, 'mousemove', function (e) {
// when resizing
if (attr(table, 'data-column-resizing') || attr(table, 'data-row-resizing')) {
return;
}
// when rezie knob area
var cursor = getCursorPosition(e),
isInArea = null;
each(table.tHead.rows, function (row) {
// out of header area
if (!options.anyWhere) {
var rowStartY = getAbsolutePosition(row).y;
if (cursor.y < rowStartY || rowStartY + row.offsetHeight < cursor.y) {
return;// continue;
}
}
// check each cells
each(row.cells, function (cell, i) {
var cr = getAbsolutePosition(cell).x + cell.offsetWidth,
center = (cr + (row.cells[i + 1] ? getAbsolutePosition(row.cells[i + 1]).x : cr)) / 2;
if (center - threshold <= cursor.x && cursor.x <= center + threshold) {
isInArea = true;
targetCell = cell;
return false;// break;
}
});
if (isInArea) {
return false;// break;
}
});
attr(table, 'data-column-resizable', isInArea);
});
// resize handler switch
setEvent(table, 'mousedown', function () {
if (targetCell && attr(table, 'data-column-resizable')) {
attr(table, 'data-column-resizing', true);
}
});
setEvent(document.body, 'mouseup', function () {
if (attr(table, 'data-column-resizing')) {
attr(table, {
'data-column-resizable': null,
'data-column-resizing': null
});
}
});
// resize handler
setEvent(document.body, 'mousemove', function (e) {
if (attr(table, 'data-column-resizing')) {
self.columnResize(targetCell, Math.max(minWidth, getCursorPosition(e).x - getAbsolutePosition(targetCell).x));
}
});
// init detect width
each(table.tHead.rows[0].children, function (th) {
th.style.width = th.offsetWidth + 'px';
});
};
customtable.prototype.columnResize = function (colIndex, colWidth) {
// parse arguments
var self = this,
cols = [];
if (arguments.length === 1 && colIndex instanceof Array) {
cols = colIndex;
} else if (arguments.length === 2) {
cols[0] = [colIndex, colWidth];
} else {
return this;
}
// set width
each(cols, function (c) {
if (!c || c.length !== 2 || isNaN(c[1])) {
return;// continue
}
var targetCell;
if (c[0] && (c[0].nodeName === 'TH' || c[0].nodeName === 'TD')) {
targetCell = c[0];
} else if (self.table.tHead.rows[0].cells[c[0]]) {
targetCell = self.table.tHead.rows[0].cells[c[0]];
} else {
return;// continue
}
var sp = targetCell.offsetWidth - getStyle(targetCell, 'width', true);
targetCell.style.width = Math.max(0, +c[1] - sp) + 'px';
});
// end
return this;
};
//
// row resizable
//
customtable.rowResizable = function (table, options) {
var self = this,
table = self.table,
options = self.options.rowResizable,
threshold = isNaN(options.threshold) ? 3 : +options.threshold,
minHeight = isNaN(options.minHeight) ? 17 : +options.minHeight,
targetRow = null;
setEvent(table, 'mousemove', function (e) {
// when resizing
if (attr(table, 'data-column-resizing') || attr(table, 'data-row-resizing')) {
return;
}
var cursor = getCursorPosition(e),
isInArea = null;
try {
var firstcol = table.rows[0].cells[0],
startX = getAbsolutePosition(firstcol).x;
if (options.anyWhere || (startX <= cursor.x && cursor.x <= startX + firstcol.offsetWidth)) {
each(table.rows, function (row, i) {
var rb = getAbsolutePosition(row).y + row.offsetHeight,
center = (rb + (table.rows[i + 1] ? getAbsolutePosition(table.rows[i + 1]).y : rb)) / 2;
if (center - threshold <= cursor.y && cursor.y <= center + threshold) {
isInArea = true;
targetRow = row;
return false;// break
}
});
}
} catch (e) {
}
attr(table, 'data-row-resizable', isInArea);
});
// resize handler switch
setEvent(table, 'mousedown', function () {
if (attr(table, 'data-row-resizable') && targetRow) {
attr(table, 'data-row-resizing', true);
}
});
setEvent(document.body, 'mouseup', function (e) {
if (attr(table, 'data-row-resizing')) {
attr(table, {
'data-row-resizable': null,
'data-row-resizing': null
});
}
});
// resize handler
setEvent(document.body, 'mousemove', function (e) {
if (attr(table, 'data-row-resizing')) {
self.rowResize(targetRow, Math.max(minHeight, getCursorPosition(e).y - getAbsolutePosition(targetRow).y));
}
});
};
customtable.prototype.rowResize = function (rowIndex, rowHeight) {
// parse arguments
var self = this,
rows = [];
if (arguments.length === 1 && rowIndex instanceof Array) {
rows = rowIndex;
} else if (arguments.length === 2) {
rows[0] = [rowIndex, rowHeight];
} else {
return this;
}
// set height
each(rows, function (r) {
if (!r || r.length !== 2 || isNaN(r[1])) {
return;// continue
}
var targetRow;
if (r[0] && r[0].nodeName === 'TR') {
targetRow = r[0];
} else if (self.table.rows[r[0]]) {
targetRow = self.table.rows[r[0]];
} else {
return;// continue
}
var sp = targetRow.offsetHeight - getStyle(targetRow, 'height', true);
targetRow.style.height = Math.max(0, +r[1] - sp) + 'px';
});
// end
return this;
};
//
// editable
//
customtable.editable = function () {
var self = this;
setEvent(self.table, 'mouseup', function (e) {
var td = e.target || e.srcElement;
if (
!td || td.nodeName !== 'TD' || td.className === 'editing'
|| attr(self.table, 'data-column-resizing')
|| attr(self.table, 'data-row-resizing')
) {
return;
}
var tr = td.parentElement,
editorTD = document.createElement('td');
// create editor
editorTD.className = 'editing';
var editorTextArea = document.createElement('textarea');
editorTextArea.value = getTextContent(td);
editorTextArea.style.height = td.offsetHeight + 'px';
editorTD.appendChild(editorTextArea);
// replace td element
tr.replaceChild(editorTD, td);
editorTextArea.focus();
// event
setEvent(editorTextArea, 'blur', function () {
// recover td
var newValue = editorTextArea.value;
if ('textContent' in td) {
td.textContent = newValue;
} else if ('innerText' in td) {
td.innerText = newValue;
} else {
td.innerHTML = newValue;
}
tr.replaceChild(td, editorTD);
});
});
};
//
// exports
//
window.customtable = customtable;
})();
.customtable.sortable thead th { padding-right: 12px; }
.customtable.sortable thead th[data-sort-direction='asc'] {
background: url(asc.png) no-repeat 95% 50%;
}
.customtable.sortable thead th[data-sort-direction='desc'] {
background: url(desc.png) no-repeat 95% 50%;
}
.customtable.columnResizable,
.customtable.rowResizable {
table-layout: fixed;
width: 0;
}
.customtable.columnResizable th,
.customtable.columnResizable td,
.customtable.rowResizable th,
.customtable.rowResizable td {
overflow: hidden;
}
.customtable[data-column-resizing],
.customtable[data-row-resizing] {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.customtable[data-column-resizable] {
cursor: w-resize;
}
.customtable[data-row-resizable] {
cursor: n-resize;
}
.customtable[data-column-resizable][data-row-resizable] {
cursor: nw-resize;
}
.customtable.editable td.editing {
padding: 0;
height: 100%;
}
.customtable.editable td.editing textarea {
font-family: inherit;
font-size: inherit;
width: 100%;
margin: 0;
padding: 0;
border: 0;
background-color: #eef9ff;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment