From 0361efc2e32dec31d7e84fd319c15af164dced8e Mon Sep 17 00:00:00 2001 From: mkoryak Date: Wed, 3 Jun 2020 11:22:42 -0400 Subject: [PATCH] added jquery wrapper and floatthead adapter --- .gitignore | 3 +- README.md | 27 ++ demo/index.html | 44 ++- ...otate-table-headings.floatthead-adapter.js | 21 ++ dist/jquery.rotate-table-headings.js | 255 ++++++++++++++++++ ...ngs-global.js => rotate-table-headings.js} | 25 +- package.json | 2 +- rollup.config.js | 28 +- src/floatthead.js | 14 + src/insert-styles.js | 3 + src/jquery.js | 62 ++++- src/rotate-table-headings.js | 20 +- 12 files changed, 466 insertions(+), 38 deletions(-) create mode 100644 dist/jquery.rotate-table-headings.floatthead-adapter.js create mode 100644 dist/jquery.rotate-table-headings.js rename dist/{rotate-table-headings-global.js => rotate-table-headings.js} (87%) create mode 100644 src/floatthead.js diff --git a/.gitignore b/.gitignore index 44d414b..f174fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .idea/ -.vcs \ No newline at end of file +.vcs +.DS_Store diff --git a/README.md b/README.md index 34944cd..537b0e2 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,33 @@ It rotates your table headings so that you can fit more stuff into the table. WORK IN PROGRESS, based on this [code](https://codepen.io/mkoryak/pen/yLNVQVE) but better ;) +## Using with floatthead + +The library comes with a jquery wrapper and an adapter that lets you run it alongside floatthead. + +Since this lib is not on a CDN, I am using relative import paths in this example that may not match yours. + +```html + + + + + +``` + +To use with floatthead, make sure you call rotate-table-headings **first**, then floatthead: + +```js +$("table.demo").rotateTableHeadings({ + maxCellHeight: 200, + angle: 45, +}).floatThead({ + scrollContainer: function($table) { + return $table.closest(".table-container"); + }, + position: "absolute" +}); +``` ## Sponsored by [Ctrl O](https://ctrlo.com) diff --git a/demo/index.html b/demo/index.html index cd0a9d0..789e41b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -7,9 +7,9 @@ - - - + + + + diff --git a/dist/jquery.rotate-table-headings.floatthead-adapter.js b/dist/jquery.rotate-table-headings.floatthead-adapter.js new file mode 100644 index 0000000..fef943c --- /dev/null +++ b/dist/jquery.rotate-table-headings.floatthead-adapter.js @@ -0,0 +1,21 @@ +(function () { + 'use strict'; + + /** + * jQuery.floatThead adapter for rotate-table-headings. + * + * Import this script after jQuery.floatThead and jQuery.rotate-table-headings. + * Run rotate-table-headings on a table first, followed by floatThead. + * + * Its kinda hacky since it requires you to never set the default options it overrides. + */ + Object.assign(jQuery.rotateTableHeadings.defaults, {callback: function ($table, lastCellWidth) { + // Ensure there is space for the last cell to extend into. + // Do it as a fake cell to placate floatThead's needs. + $table.find('tr').append($("")); + }}); + Object.assign(jQuery.floatThead.defaults, { + floatContainerClass: 'rotate-table-headings-container' + }); + +}()); diff --git a/dist/jquery.rotate-table-headings.js b/dist/jquery.rotate-table-headings.js new file mode 100644 index 0000000..b60509d --- /dev/null +++ b/dist/jquery.rotate-table-headings.js @@ -0,0 +1,255 @@ +(function () { + 'use strict'; + + /** + * Inserts styles that are required by this library into the DOM. + */ + function insertStyles () { + var STYLE_ID = 'rotate-table-headings-style'; + + if (document.getElementById(STYLE_ID)) { + return; + } + + var styleElem = document.createElement('style'); + styleElem.type = 'text/css'; + styleElem.id = STYLE_ID; + document.head.appendChild(styleElem); + var sheet = styleElem.sheet; + + var stringify = function (selector, rules) { + return (selector + " {\n" + (Object.keys(rules) + .map(function (key) { return ((key.split(/(?=[A-Z])/).join('-').toLowerCase()) + ": " + (rules[key]) + ";"); }).join('\n')) + "\n}"); + }; + + var add = function (selector, rules) { + sheet.insertRule(stringify(selector, rules), + sheet.cssRules.length); + }; + + add(".rotate-table-headings-container", { + overflowY: 'hidden', + }); + add("table.rotate-table-headings", { + borderCollapse: 'collapse', + borderSpacing: '0', + }); + add("table.rotate-table-headings .cell-rotate", { + verticalAlign: 'bottom', + padding: '0 !important', + textAlign: 'left', + }); + add("table.rotate-table-headings .cell-rotate .cell-positioner", { + position: 'relative', + }); + add("table.rotate-table-headings .cell-rotate .cell-label", { + position: 'absolute', + bottom: '0', + textAlign: 'left', + left: '100%', + transformOrigin: 'bottom left', + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }); + } + + /** + * Rotates the contents of table cells by `angle` while also + * truncating their contents if it does not fit inside the max + * cell height. + * + * @param {!NodeList} tableCells NodeList of table cells: TDs + * or THs to operate on. + * @param {number} maxCellHeight Maximum height of the cell in pixels. + * Used for truncating the cell contents to fit. Use a very large + * number if you do not want to truncate cell text. + * @param {boolean=} setLastCellWidth Whether to set the last cell's + * width so that the header extends outside the table. + * @param {number=} angle Angle in degrees. + * @param {number=} textTruncateOffset Subtract this from the max width + * used to truncate cell text. It is needed because our forumula does + * not take font-size into account. + * + * @return {number} The horizontal width of the last cell's label. Since + * this cell will extend outside of the table, you will need this value + * to add padding to the table if the label goes off-screen. + */ + function rotateTableHeadings( + tableCells, + maxCellHeight, + setLastCellWidth, + angle, + textTruncateOffset + ){ + if ( setLastCellWidth === void 0 ) setLastCellWidth = true; + if ( angle === void 0 ) angle = 45; + if ( textTruncateOffset === void 0 ) textTruncateOffset = 10; + + var solveRightTriangle = function (ref) { + var hypotenuse = ref.hypotenuse; + var height = ref.height; + + var rads = (angle * Math.PI) / 180; + if (hypotenuse) { + // Solve for height. + return hypotenuse * Math.sin(rads); + } else { + var width = height / Math.tan(rads); + // Solve for hypotenuse. + return Math.sqrt(Math.pow(height, 2) + Math.pow(width, 2)); + } + }; + + var crel = function (clazz, name) { + if ( name === void 0 ) name = 'div'; + + var el = document.createElement(name); + el.classList.add(clazz); + return el; + }; + var empty = function (el) { + while(el.firstChild) { + el.removeChild(el.firstChild); + } + return el; + }; + var findParentTable = function (el) { + while (el) { + if (el.nodeName === 'TABLE') { + return el; + } else { + el = el.parentElement; + } + } + }; + + // Push the cell down to line the border up with the columns. + // Equal to border-bottom-width. + var compensateForCellBorder = function (cellLabel) { + cellLabel.style.marginBottom = + "-" + (getComputedStyle(cellLabel).borderBottomWidth); + }; + + if(tableCells.length === 0){ + return 0; + } + + // This method ensures that styles are inserted only once. + insertStyles(); + + var table = findParentTable(tableCells[0]); + table.classList.add('rotate-table-headings'); + + var maxHeadingWidth = + solveRightTriangle({ height: maxCellHeight }) - textTruncateOffset; + + var maxWidth = -1; + var lastCellWidth = 0; + var cellLabels = []; + + for(var i = 0, list = tableCells; i < list.length; i += 1){ + var cell = list[i]; + + cell.style.whiteSpace = 'nowrap'; + var hypotenuse = cell.offsetWidth; + var text = cell.innerText; + var height = solveRightTriangle({hypotenuse: hypotenuse}); + var idealHeight = Math.min(height, maxCellHeight); + maxWidth = Math.max(hypotenuse, maxWidth); + + cell.style.height = idealHeight + 'px'; + cell.setAttribute('aria-label', text); + var cellLabel = crel('cell-label'); + cellLabel.innerText = text; + cellLabel.setAttribute('label', text); + cellLabel.style.transform = "rotate(" + (360 - angle) + "deg)"; + cellLabel.style.maxWidth = maxHeadingWidth + 'px'; + var positioner = crel('cell-positioner'); + positioner.appendChild(cellLabel); + empty(cell).appendChild(positioner); + cell.classList.add('cell-rotate'); + cellLabels.push(cellLabel); + + if(cell === tableCells[tableCells.length - 1]) { + lastCellWidth = height / Math.tan(angle * Math.PI / 180); + } + } + if(!setLastCellWidth) { + var lastLabel = cellLabels.pop(); + compensateForCellBorder(lastLabel); + } + for(var i$1 = 0, list$1 = cellLabels; i$1 < list$1.length; i$1 += 1){ + var cellLabel$1 = list$1[i$1]; + + cellLabel$1.style.width = maxWidth + 'px'; + compensateForCellBorder(cellLabel$1); + } + return lastCellWidth - textTruncateOffset; + } + + /** + * JQuery wrapper for rotateTableHeadings. + * You may run this plugin on a table wrapped set or a header cell + * wrapped set. ex: + * $('table.rotated').rotateTableHeadings(); + * $('table tr.rotated > th').rotateTableHeadings(); + * + * The options object has: + * @param {number} maxCellHeight Maximum height of the cell in pixels. + * Used for truncating the cell contents to fit. Use a very large + * number if you do not want to truncate cell text. + * @param {boolean=} setLastCellWidth Whether to set the last cell's + * width so that the header extends outside the table. + * @param {number=} angle Angle in degrees. + * @param {number=} textTruncateOffset Subtract this from the max width + * used to truncate cell text. It is needed because our forumula does + * not take font-size into account. + * + * @param {string=} cellSelector If you run this plugin on a table, this + * selector will be used to find the table cells to rotate. + * @param {function($table: jQuery, lastCellWidth: number): jQuery} callback + * A function executed after the plugin runs giving you access to the + * table and the lastCellWidth. + */ + jQuery.fn.rotateTableHeadings = function(options) { + if ( options === void 0 ) options = {}; + + var opts = Object.assign(jQuery.rotateTableHeadings.defaults, options); + var $this = this; + + var wrapper = function ($cells) { + $cells = $cells.filter('th,td'); + var lastCellWidth = rotateTableHeadings( + $cells.toArray(), + opts.maxCellHeight, + opts.setLastCellWidth, + opts.angle, + opts.textTruncateOffset + ); + opts.callback($cells.closest('table'), lastCellWidth); + }; + + if($this.length && $this[0].nodeName === 'TABLE') { + // Assume that this is a list of tables. + $this.each(function(){ + wrapper($this.find(opts.cellSelector)); + }); + } else { + wrapper($this); + } + // This is required for making jQuery plugin calls chainable. + return $this; + }; + jQuery.rotateTableHeadings = { + defaults: { + maxCellHeight: 5000, + setLastCellWidth: true, + angle: 45, + textTruncateOffset: 10, + cellSelector: '> thead th', + callback: function ($table, lastCellWidth) {}, + } + }; + +}()); diff --git a/dist/rotate-table-headings-global.js b/dist/rotate-table-headings.js similarity index 87% rename from dist/rotate-table-headings-global.js rename to dist/rotate-table-headings.js index edd8248..1b7adaa 100644 --- a/dist/rotate-table-headings-global.js +++ b/dist/rotate-table-headings.js @@ -27,6 +27,9 @@ sheet.cssRules.length); }; + add(".rotate-table-headings-container", { + overflowY: 'hidden', + }); add("table.rotate-table-headings", { borderCollapse: 'collapse', borderSpacing: '0', @@ -61,6 +64,8 @@ * @param {number} maxCellHeight Maximum height of the cell in pixels. * Used for truncating the cell contents to fit. Use a very large * number if you do not want to truncate cell text. + * @param {boolean=} setLastCellWidth Whether to set the last cell's + * width so that the header extends outside the table. * @param {number=} angle Angle in degrees. * @param {number=} textTruncateOffset Subtract this from the max width * used to truncate cell text. It is needed because our forumula does @@ -73,9 +78,11 @@ function rotateTableHeadings( tableCells, maxCellHeight, + setLastCellWidth, angle, textTruncateOffset ){ + if ( setLastCellWidth === void 0 ) setLastCellWidth = true; if ( angle === void 0 ) angle = 45; if ( textTruncateOffset === void 0 ) textTruncateOffset = 10; @@ -116,6 +123,14 @@ } } }; + + // Push the cell down to line the border up with the columns. + // Equal to border-bottom-width. + var compensateForCellBorder = function (cellLabel) { + cellLabel.style.marginBottom = + "-" + (getComputedStyle(cellLabel).borderBottomWidth); + }; + if(tableCells.length === 0){ return 0; } @@ -160,18 +175,20 @@ lastCellWidth = height / Math.tan(angle * Math.PI / 180); } } + if(!setLastCellWidth) { + var lastLabel = cellLabels.pop(); + compensateForCellBorder(lastLabel); + } for(var i$1 = 0, list$1 = cellLabels; i$1 < list$1.length; i$1 += 1){ var cellLabel$1 = list$1[i$1]; cellLabel$1.style.width = maxWidth + 'px'; - - // Push the cell down to line the border up with the columns. - // Equal to border-bottom-width. - cellLabel$1.style.marginBottom = "-" + (getComputedStyle(cellLabel$1).borderBottomWidth); + compensateForCellBorder(cellLabel$1); } return lastCellWidth - textTruncateOffset; } + // Rollup entry point to generate an IIFE with a global on window. window.rotateTableHeadings = rotateTableHeadings; }()); diff --git a/package.json b/package.json index 56e02a3..b23d8c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rotate-table-headings", - "version": "0.1.0", + "version": "0.2.0", "description": "Rotates the table headings", "main": "index.js", "scripts": { diff --git a/rollup.config.js b/rollup.config.js index 7d6e39b..853b5d4 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,13 +1,35 @@ import buble from '@rollup/plugin-buble'; -export default { +export default [{ input: 'src/global.js', output: [{ - file: 'dist/rotate-table-headings-global.js', + file: 'dist/rotate-table-headings.js', format: 'iife', }], compact: true, plugins: [buble({transforms: { dangerousForOf: true, }})] -}; \ No newline at end of file +}, +{ + input: 'src/jquery.js', + output: [{ + file: 'dist/jquery.rotate-table-headings.js', + format: 'iife', + }], + compact: true, + plugins: [buble({transforms: { + dangerousForOf: true, + }})] +}, +{ + input: 'src/floatthead.js', + output: [{ + file: 'dist/jquery.rotate-table-headings.floatthead-adapter.js', + format: 'iife', + }], + compact: true, + plugins: [buble({transforms: { + dangerousForOf: true, + }})] +}]; \ No newline at end of file diff --git a/src/floatthead.js b/src/floatthead.js new file mode 100644 index 0000000..0084b63 --- /dev/null +++ b/src/floatthead.js @@ -0,0 +1,14 @@ +/** + * jQuery.floatThead adapter for rotate-table-headings. + * + * Import this script after jQuery.floatThead and jQuery.rotate-table-headings. + * Run rotate-table-headings on a table first, followed by floatThead. + * + * Its kinda hacky since it requires you to never set the default options it overrides. + */ + Object.assign(jQuery.rotateTableHeadings.defaults, {callback: ($table, lastCellWidth) => { + // Ensure there is space for the last cell to extend into. + // Do it as a fake cell to placate floatThead's needs. + $table.find('tr').append(``); + }}); + Object.assign(jQuery.floatThead.defaults, {floatContainerClass: 'rotate-table-headings-container'}); \ No newline at end of file diff --git a/src/insert-styles.js b/src/insert-styles.js index 381fd5c..61b98c0 100644 --- a/src/insert-styles.js +++ b/src/insert-styles.js @@ -26,6 +26,9 @@ export default function () { sheet.cssRules.length); }; + add(".rotate-table-headings-container", { + overflowY: 'hidden', + }); add("table.rotate-table-headings", { borderCollapse: 'collapse', borderSpacing: '0', diff --git a/src/jquery.js b/src/jquery.js index 5874b79..dadbca5 100644 --- a/src/jquery.js +++ b/src/jquery.js @@ -1,5 +1,63 @@ import rotateTableHeadings from './rotate-table-headings'; -// TODO: Rollup entry point to generate a jquery wrapper with a global on window. - +/** + * JQuery wrapper for rotateTableHeadings. + * You may run this plugin on a table wrapped set or a header cell + * wrapped set. ex: + * $('table.rotated').rotateTableHeadings(); + * $('table tr.rotated > th').rotateTableHeadings(); + * + * The options object has: + * @param {number} maxCellHeight Maximum height of the cell in pixels. + * Used for truncating the cell contents to fit. Use a very large + * number if you do not want to truncate cell text. + * @param {boolean=} setLastCellWidth Whether to set the last cell's + * width so that the header extends outside the table. + * @param {number=} angle Angle in degrees. + * @param {number=} textTruncateOffset Subtract this from the max width + * used to truncate cell text. It is needed because our forumula does + * not take font-size into account. + * + * @param {string=} cellSelector If you run this plugin on a table, this + * selector will be used to find the table cells to rotate. + * @param {function($table: jQuery, lastCellWidth: number): jQuery} callback + * A function executed after the plugin runs giving you access to the + * table and the lastCellWidth. + */ +jQuery.fn.rotateTableHeadings = function(options = {}) { + const opts = Object.assign(jQuery.rotateTableHeadings.defaults, options); + const $this = this; + const wrapper = ($cells) => { + $cells = $cells.filter('th,td'); + const lastCellWidth = rotateTableHeadings( + $cells.toArray(), + opts.maxCellHeight, + opts.setLastCellWidth, + opts.angle, + opts.textTruncateOffset, + ); + opts.callback($cells.closest('table'), lastCellWidth); + }; + + if($this.length && $this[0].nodeName === 'TABLE') { + // Assume that this is a list of tables. + $this.each(function(){ + wrapper($this.find(opts.cellSelector)); + }); + } else { + wrapper($this); + } + // This is required for making jQuery plugin calls chainable. + return $this; +}; +jQuery.rotateTableHeadings = { + defaults: { + maxCellHeight: 5000, + setLastCellWidth: true, + angle: 45, + textTruncateOffset: 10, + cellSelector: '> thead th', + callback: ($table, lastCellWidth) => {}, + } +} \ No newline at end of file diff --git a/src/rotate-table-headings.js b/src/rotate-table-headings.js index 46e35d4..0a1ff4c 100644 --- a/src/rotate-table-headings.js +++ b/src/rotate-table-headings.js @@ -10,6 +10,8 @@ import insertStyles from './insert-styles'; * @param {number} maxCellHeight Maximum height of the cell in pixels. * Used for truncating the cell contents to fit. Use a very large * number if you do not want to truncate cell text. + * @param {boolean=} setLastCellWidth Whether to set the last cell's + * width so that the header extends outside the table. * @param {number=} angle Angle in degrees. * @param {number=} textTruncateOffset Subtract this from the max width * used to truncate cell text. It is needed because our forumula does @@ -22,6 +24,7 @@ import insertStyles from './insert-styles'; export default function( tableCells, maxCellHeight, + setLastCellWidth = true, angle = 45, textTruncateOffset = 10 ){ @@ -57,6 +60,14 @@ export default function( } } }; + + // Push the cell down to line the border up with the columns. + // Equal to border-bottom-width. + const compensateForCellBorder = (cellLabel) => { + cellLabel.style.marginBottom = + `-${getComputedStyle(cellLabel).borderBottomWidth}`; + }; + if(tableCells.length === 0){ return 0; } @@ -99,12 +110,13 @@ export default function( lastCellWidth = height / Math.tan(angle * Math.PI / 180); } } + if(!setLastCellWidth) { + const lastLabel = cellLabels.pop(); + compensateForCellBorder(lastLabel); + } for(const cellLabel of cellLabels){ cellLabel.style.width = maxWidth + 'px'; - - // Push the cell down to line the border up with the columns. - // Equal to border-bottom-width. - cellLabel.style.marginBottom = `-${getComputedStyle(cellLabel).borderBottomWidth}`; + compensateForCellBorder(cellLabel); } return lastCellWidth - textTruncateOffset; }; \ No newline at end of file