svelte-resizable-columns
Version:
Svelte JS action for resizing HTML table columns
423 lines (349 loc) • 14.4 kB
JavaScript
const DATA_API = 'resizableColumns';
const DATA_COLUMNS_ID = 'resizable-columns-id';
const DATA_COLUMN_ID = 'resizable-column-id';
const DATA_TH = 'th';
const CLASS_TABLE_RESIZING = 'rc-table-resizing';
const CLASS_COLUMN_RESIZING = 'rc-column-resizing';
const CLASS_HANDLE = 'rc-handle';
const CLASS_HANDLE_NORESIZE = 'rc-handle-noresize';
const CLASS_HANDLE_CONTAINER = 'rc-handle-container';
const EVENT_RESIZE_START = 'column:resize:start';
const EVENT_RESIZE = 'column:resize';
const EVENT_RESIZE_STOP = 'column:resize:stop';
const SELECTOR_UNRESIZABLE = `[data-noresize]`;
function setWidth(element, width) {
width = width.toFixed(2);
width = width > 0 ? width : 0;
element.style.width = width + '%';
}
function setWidthPx(element, width) {
width = width.toFixed(2);
width = width > 0 ? width : 0;
element.style.width = width + 'px';
}
function setHeightPx(element, height) {
height = height.toFixed(2);
height = height > 0 ? height : 0;
element.style.height = height + 'px';
}
function setLeftPx(element, left) {
left = left.toFixed(2);
left = left > 0 ? left : 0;
element.style.left = left + 'px';
}
function getPointerX(event) {
if (event.type.indexOf('touch') === 0) {
return (event.originalEvent.touches[0] || event.originalEvent.changedTouches[0]).pageX;
}
return event.pageX;
}
function parseWidth(element) {
return element ? parseFloat(element.style.width.replace('%', '')) : 0;
}
function indexOfElementInParent(el) {
if(el && el.parentNode && el.parentNode.children) {
const children = el.parentNode.children,
numItems = children.length;
for (let index = 0; index < children.length; index++) {
const element = children[index];
if(element === el) {
return index;
}
}
}
return -1;
}
function isVisible(el) {
return el && el.style &&
el.style.display !== 'none' &&
el.style.visible !== 'hidden' &&
(!el.type || el.type !== 'hidden');
}
function ResizableColumns(table, options) {
if(!table || !table.tagName || table.tagName !== 'TABLE') {
console.error('The ResizableColumns action applies only to table DOM elements.');
}
table.ResizableColumns = this;
this.options = Object.assign({}, ResizableColumns.defaults, options);
if(!this.options.hasOwnProperty('store')) {
this.options.store= window ? window.store : null;
}
this.window = window;
this.ownerDocument = table.ownerDocument;
this.table = table;
this.handleContainer;
this.tableHeaders;
this.syncHandleWidthsCallback = this.syncHandleWidths.bind(this);
this.onPointerMoveCallback = this.onPointerMove.bind(this);
this.onPointerUpCallback = this.onPointerUp.bind(this);
this.onPointerDownCallback = this.onPointerDown.bind(this);
this.onPointerMoveCallback = this.onPointerMove.bind(this);
this.refreshHeaders();
this.restoreColumnWidths();
this.syncHandleWidths();
// bindEvents(this.window, 'resize', this.syncHandleWidths.bind(this));
this.window.addEventListener('resize', this.syncHandleWidthsCallback);
return {
update(value) {
},
destroy() {
let self = table.ResizableColumns;
table.ResizableColumns = null;
let handles = self.handleContainer.querySelectorAll('.'+CLASS_HANDLE);
self.window.removeEventListener('resize', self.syncHandleWidthsCallback);
handles.forEach(el => {
el.removeEventListener('mousedown', self.onPointerDownCallback);
el.removeEventListener('touchstart', self.onPointerDownCallback);
});
self.operation = null;
self.handleContainer.parentNode.removeChild(self.handleContainer);
self.handleContainer = null;
self.tableHeaders = null;
self.table = null;
self.ownerDocument = null;
self.window = null;
self.options = null;
}
};
}
ResizableColumns.prototype = {
createHandles: function() {
let ref = this.handleContainer;
if (ref != null) {
ref.parentNode.removeChild(ref);
}
this.handleContainer = document.createElement('div');
this.handleContainer.className = CLASS_HANDLE_CONTAINER;
this.table.parentNode.insertBefore(this.handleContainer, this.table);
for (let i = 0; i < (this.tableHeaders.length-1); i++) {
const el = this.tableHeaders[i];
let current = this.tableHeaders[i];
let next = this.tableHeaders[i + 1];
let handle = document.createElement('div');
if (!next || current.matches(SELECTOR_UNRESIZABLE) || next.matches(SELECTOR_UNRESIZABLE)) {
handle.className = CLASS_HANDLE_NORESIZE;
} else {
handle.className = CLASS_HANDLE;
}
this.handleContainer.appendChild(handle);
};
let handles = this.handleContainer.querySelectorAll('.'+CLASS_HANDLE);
handles.forEach(el => {
el.addEventListener('mousedown', this.onPointerDownCallback);
el.addEventListener('touchstart', this.onPointerDownCallback);
});
},
assignPercentageWidths: function() {
this.tableHeaders.forEach((el, _) => {
setWidth(el, el.offsetWidth / this.table.offsetWidth * 100);
});
},
refreshHeaders: function() {
// Allow the selector to be both a regular selctor string as well as
// a dynamic callback
let getHeaders = this.options.getHeaders;
if(typeof getHeaders !== 'function') {
console.error('ResizableColumns: options.getHeaders requires a function that returns the list of headers');
return;
}
this.tableHeaders = Array.from(getHeaders.call(null, this.table)).filter(el => isVisible(el));
// Assign percentage widths first, then create drag handles
this.assignPercentageWidths();
this.createHandles();
},
restoreColumnWidths: function() {
this.tableHeaders.forEach((el, _) => {
if(this.options.store && !el.matches(SELECTOR_UNRESIZABLE)) {
let width = this.options.store.get(
this.generateColumnId(el)
);
if(width != null) {
setWidth(el, width);
}
}
});
},
generateColumnId: function($el) {
return this.table.data(DATA_COLUMNS_ID) + '-' + $el.data(DATA_COLUMN_ID);
},
constrainWidth: function(width) {
if (this.options.minWidth != undefined) {
width = Math.max(this.options.minWidth, width);
}
if (this.options.maxWidth != undefined) {
width = Math.min(this.options.maxWidth, width);
}
return width;
},
saveColumnWidths: function() {
this.tableHeaders.forEach((el, _) => {
if (this.options.store && !el.matches(SELECTOR_UNRESIZABLE)) {
this.options.store.set(
this.generateColumnId(el),
parseWidth(el)
);
}
});
},
syncHandleWidths: function() {
let container = this.handleContainer;
setWidthPx(container, this.table.offsetWidth);
let height = this.options.resizeFromBody ?
this.table.offsetHeight :
this.table.querySelector('thead').offsetHeight;
container
.querySelectorAll('div')
// .querySelectorAll('.'+CLASS_HANDLE)
.forEach((el, i) => {
let header = this.tableHeaders[i];
let left = header.clientWidth + (
header.offsetLeft - this.handleContainer.offsetLeft
) + parseInt(getComputedStyle(this.handleContainer.parentNode).paddingLeft);
setHeightPx(el, height);
setLeftPx(el, left);
});
},
onPointerDown: function(event) {
// Only applies to left-click dragging
if(event.which !== 1) { return; }
// If a previous operation is defined, we missed the last mouseup.
// Probably gobbled up by user mousing out the window then releasing.
// We'll simulate a pointerup here prior to it
if(this.operation) {
this.onPointerUp(event);
}
// Ignore non-resizable columns
let currentGrip = event.currentTarget;
if(currentGrip.matches(SELECTOR_UNRESIZABLE)) {
return;
}
let gripIndex = indexOfElementInParent(currentGrip);
let leftColumn = this.tableHeaders[gripIndex];
if(leftColumn && leftColumn.matches(SELECTOR_UNRESIZABLE)) {
leftColumn = null;
}
let rightColumn = this.tableHeaders[gripIndex + 1];
if(rightColumn && rightColumn.matches(SELECTOR_UNRESIZABLE)) {
rightColumn = null;
}
let leftWidth = parseWidth(leftColumn);
let rightWidth = parseWidth(rightColumn);
this.operation = {
leftColumn, rightColumn, currentGrip,
startX: getPointerX(event),
widths: {
left: leftWidth,
right: rightWidth
},
newWidths: {
left: leftWidth,
right: rightWidth
}
};
this.ownerDocument.addEventListener('mousemove', this.onPointerMoveCallback);
this.ownerDocument.addEventListener('touchmove', this.onPointerMoveCallback);
this.ownerDocument.addEventListener('mouseup', this.onPointerUpCallback);
this.ownerDocument.addEventListener('touchend', this.onPointerUpCallback);
this.handleContainer.className += ` ${CLASS_TABLE_RESIZING}`;
this.table.className += ` ${CLASS_TABLE_RESIZING}`;
if(leftColumn) leftColumn.className += ` ${CLASS_COLUMN_RESIZING}`;
if(rightColumn) rightColumn.className += ` ${CLASS_COLUMN_RESIZING}`;
currentGrip.className += ` ${CLASS_COLUMN_RESIZING}`;
this.table.dispatchEvent(
new CustomEvent('resize-columns-start', {
detail: {
leftColumn,
rightColumn,
leftWidth,
rightWidth
},
})
);
event.preventDefault();
},
onPointerMove: function(event) {
let op = this.operation;
if(!this.operation) { return; }
// Determine the delta change between start and new mouse position, as a percentage of the table width
let difference = (getPointerX(event) - op.startX) / this.table.offsetWidth * 100;
if(difference === 0) {
return;
}
let leftColumn = op.leftColumn;
let rightColumn = op.rightColumn;
let widthLeft, widthRight;
if(difference > 0) {
widthLeft = this.constrainWidth(op.widths.left + (op.widths.right - op.newWidths.right));
widthRight = this.constrainWidth(op.widths.right - difference);
}
else if(difference < 0) {
widthLeft = this.constrainWidth(op.widths.left + difference);
widthRight = this.constrainWidth(op.widths.right + (op.widths.left - op.newWidths.left));
}
if(leftColumn) {
setWidth(leftColumn, widthLeft);
}
if(rightColumn) {
setWidth(rightColumn, widthRight);
}
op.newWidths.left = widthLeft;
op.newWidths.right = widthRight;
this.table.dispatchEvent(
new CustomEvent('resize-columns-move', {
detail: {
leftColumn: op.leftColumn,
rightColumn: op.rightColumn,
leftWidth: widthLeft,
rightWidth: widthRight
},
})
);
},
onPointerUp: function(event) {
let op = this.operation;
if(!this.operation) { return; }
this.ownerDocument.removeEventListener('mousemove', this.onPointerMoveCallback);
this.ownerDocument.removeEventListener('touchmove', this.onPointerMoveCallback);
this.ownerDocument.removeEventListener('mouseup', this.onPointerUpCallback);
this.ownerDocument.removeEventListener('touchend', this.onPointerUpCallback);
this.handleContainer.className = this.handleContainer.className.replace(' '+CLASS_TABLE_RESIZING, '');
this.table.className = this.table.className.replace(' '+CLASS_TABLE_RESIZING, '');
if(op.leftColumn) op.leftColumn.className = op.leftColumn.className.replace(' '+CLASS_COLUMN_RESIZING, '');
if(op.rightColumn) op.rightColumn.className = op.rightColumn.className.replace(' '+CLASS_COLUMN_RESIZING, '');
op.currentGrip.className = op.currentGrip.className.replace(' '+CLASS_COLUMN_RESIZING, '');
this.syncHandleWidths();
this.saveColumnWidths();
this.operation = null;
this.table.dispatchEvent(
new CustomEvent('resize-columns-stop', {
detail: {
leftColumn: op.leftColumn,
rightColumn: op.rightColumn,
leftWidth: op.newWidths.left,
rightWidth: op.newWidths.right
},
})
);
}
}
ResizableColumns.constructor = ResizableColumns;
ResizableColumns.defaults = {
getHeaders: function(table) {
const thead = table.querySelector('thead');
if(thead) {
return thead.querySelectorAll('th');
} else {
const tr = table.querySelector('tr');
return tr.querySelectorAll('td');
}
},
// store: window.store, //created dynamically during runtime (above) to support SSR
syncHandlers: true,
resizeFromBody: true,
maxWidth: null,
minWidth: 0.01
};
ResizableColumns.count = 0;
export default function(node, options) {
return new ResizableColumns(node, options);
}