diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44d414b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.idea/ +.vcs \ No newline at end of file diff --git a/README.md b/README.md index eb4e662..34944cd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # rotate-table-headings -It rotates your table headings so that you can fit more stuff into the table +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 ;) + + +## Sponsored by [Ctrl O](https://ctrlo.com) + +Ctrl O provides simple and innovative products to help an organization's business processes. +Linkspace, its flagship product, helps share information between teams and individuals, in a simple and effective manner. diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..cd0a9d0 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,465 @@ + + + + Example + + + + + + + + + + + + +I got rid of bootstrap styles, they were making it hard to figure out what things I needed to have. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Food1 DescriptionFood2 DescriptionFood3 DescriptionFood4 DescriptionFood5 DescriptionFood6 Description7!!!!!!!!!! !!!Calories(kcal)Protein (g)Fat (g)Carbohydrate (g)Food Description and Portion SizeCalories(kcal)Protein (g)Fat (g)Carbohydrate (g)Food Description and Portion SizeCalories(kcal)Protein (g)Fat (g)Carbohydrate (g)
APPLE PIE 1 PIEAPPLE PIE 1 PIEAPPLE PIE 1 PIEAPPLE PIE 1 PIEAPPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 21212121 105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
APPLE PIE 1 PIECE40531860APPLE PIE 1 PIE242021105360APPLE PIE 1 PIE242021105360
+
+ + + + + \ No newline at end of file diff --git a/demo/styles.css b/demo/styles.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/demo/styles.css @@ -0,0 +1 @@ + diff --git a/dist/rotate-table-headings-global.js b/dist/rotate-table-headings-global.js new file mode 100644 index 0000000..edd8248 --- /dev/null +++ b/dist/rotate-table-headings-global.js @@ -0,0 +1,177 @@ +(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("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 {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, + angle, + textTruncateOffset + ){ + 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; + } + } + }; + 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); + } + } + 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); + } + return lastCellWidth - textTruncateOffset; + } + + window.rotateTableHeadings = rotateTableHeadings; + +}()); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e333d74 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,254 @@ +{ + "name": "rotate-table-headings", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@rollup/plugin-buble": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-buble/-/plugin-buble-0.21.1.tgz", + "integrity": "sha512-Tmd4V95cVyGTwh7qc9ZNkg53E/isFY4q/sqZK7mSyGajYp9Wb0gbJyZWAzYlg9kZxEHmwCDlvcHDcn56SpOCCQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.4", + "@types/buble": "^0.19.2", + "buble": "^0.19.8" + } + }, + "@rollup/pluginutils": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.8.tgz", + "integrity": "sha512-rYGeAc4sxcZ+kPG/Tw4/fwJODC3IXHYDH4qusdN/b6aLw5LPUbzpecYbEJh4sVQGPFJxd2dBU4kc1H3oy9/bnw==", + "dev": true, + "requires": { + "estree-walker": "^1.0.1" + } + }, + "@types/buble": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@types/buble/-/buble-0.19.2.tgz", + "integrity": "sha512-uUD8zIfXMKThmFkahTXDGI3CthFH1kMg2dOm3KLi4GlC5cbARA64bEcUMbbWdWdE73eoc/iBB9PiTMqH0dNS2Q==", + "dev": true, + "requires": { + "magic-string": "^0.25.0" + } + }, + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "buble": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/buble/-/buble-0.19.8.tgz", + "integrity": "sha512-IoGZzrUTY5fKXVkgGHw3QeXFMUNBFv+9l8a4QJKG1JhG3nCMHTdEX1DCOg8568E2Q9qvAQIiSokv6Jsgx8p2cA==", + "dev": true, + "requires": { + "acorn": "^6.1.1", + "acorn-dynamic-import": "^4.0.0", + "acorn-jsx": "^5.0.1", + "chalk": "^2.4.2", + "magic-string": "^0.25.3", + "minimist": "^1.2.0", + "os-homedir": "^2.0.0", + "regexpu-core": "^4.5.4" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "os-homedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-2.0.0.tgz", + "integrity": "sha512-saRNz0DSC5C/I++gFIaJTXoFJMRwiP5zHar5vV3xQ2TkgEw6hDCcU5F272JjUylpiVgBrZNQHnfjkLabTfb92Q==", + "dev": true + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", + "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regexpu-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", + "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + } + }, + "regjsgen": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", + "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", + "dev": true + }, + "regjsparser": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", + "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + }, + "rollup": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.3.2.tgz", + "integrity": "sha512-p66+fbfaUUOGE84sHXAOgfeaYQMslgAazoQMp//nlR519R61213EPFgrMZa48j31jNacJwexSAR1Q8V/BwGKBA==", + "dev": true, + "requires": { + "fsevents": "~2.1.2" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..56e02a3 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "rotate-table-headings", + "version": "0.1.0", + "description": "Rotates the table headings", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "./node_modules/rollup/dist/bin/rollup -c --watch" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mkoryak/rotate-table-headings.git" + }, + "keywords": [ + "table", + "heading", + "th", + "rotate", + "transform" + ], + "author": "Misha Koryak", + "license": "MIT", + "bugs": { + "url": "https://github.com/mkoryak/rotate-table-headings/issues" + }, + "homepage": "https://github.com/mkoryak/rotate-table-headings#readme", + "devDependencies": { + "@rollup/plugin-buble": "^0.21.1", + "rollup": "^2.3.2" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..7d6e39b --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,13 @@ +import buble from '@rollup/plugin-buble'; + +export default { + input: 'src/global.js', + output: [{ + file: 'dist/rotate-table-headings-global.js', + format: 'iife', + }], + compact: true, + plugins: [buble({transforms: { + dangerousForOf: true, + }})] +}; \ No newline at end of file diff --git a/src/global.js b/src/global.js new file mode 100644 index 0000000..5d801a1 --- /dev/null +++ b/src/global.js @@ -0,0 +1,5 @@ +import rotateTableHeadings from './rotate-table-headings'; + +// Rollup entry point to generate an IIFE with a global on window. +window.rotateTableHeadings = rotateTableHeadings; + diff --git a/src/insert-styles.js b/src/insert-styles.js new file mode 100644 index 0000000..381fd5c --- /dev/null +++ b/src/insert-styles.js @@ -0,0 +1,51 @@ +/** + * Inserts styles that are required by this library into the DOM. + */ +export default function () { + const STYLE_ID = 'rotate-table-headings-style'; + + if (document.getElementById(STYLE_ID)) { + return; + } + + const styleElem = document.createElement('style'); + styleElem.type = 'text/css'; + styleElem.id = STYLE_ID; + document.head.appendChild(styleElem); + const sheet = styleElem.sheet; + + const stringify = (selector, rules) => { + return `${ selector } {\n${ + Object.keys(rules) + .map(key => `${key.split(/(?=[A-Z])/).join('-').toLowerCase()}: ${ rules[key] };`).join('\n') + }\n}`; + }; + + const add = (selector, rules) => { + sheet.insertRule(stringify(selector, rules), + sheet.cssRules.length); + }; + + 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', + }); +} \ No newline at end of file diff --git a/src/jquery.js b/src/jquery.js new file mode 100644 index 0000000..5874b79 --- /dev/null +++ b/src/jquery.js @@ -0,0 +1,5 @@ +import rotateTableHeadings from './rotate-table-headings'; + +// TODO: Rollup entry point to generate a jquery wrapper with a global on window. + + diff --git a/src/rotate-table-headings.js b/src/rotate-table-headings.js new file mode 100644 index 0000000..46e35d4 --- /dev/null +++ b/src/rotate-table-headings.js @@ -0,0 +1,110 @@ +import insertStyles from './insert-styles'; + +/** + * 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 {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. + */ +export default function( + tableCells, + maxCellHeight, + angle = 45, + textTruncateOffset = 10 +){ + const solveRightTriangle = ({ hypotenuse, height }) => { + const rads = (angle * Math.PI) / 180; + if (hypotenuse) { + // Solve for height. + return hypotenuse * Math.sin(rads); + } else { + const width = height / Math.tan(rads); + // Solve for hypotenuse. + return Math.sqrt(Math.pow(height, 2) + Math.pow(width, 2)); + } + }; + + const crel = (clazz, name = 'div') => { + const el = document.createElement(name); + el.classList.add(clazz); + return el; + }; + const empty = (el) => { + while(el.firstChild) { + el.removeChild(el.firstChild); + } + return el; + }; + const findParentTable = (el) => { + while (el) { + if (el.nodeName === 'TABLE') { + return el; + } else { + el = el.parentElement; + } + } + }; + if(tableCells.length === 0){ + return 0; + } + + // This method ensures that styles are inserted only once. + insertStyles(); + + const table = findParentTable(tableCells[0]); + table.classList.add('rotate-table-headings'); + + const maxHeadingWidth = + solveRightTriangle({ height: maxCellHeight }) - textTruncateOffset; + + let maxWidth = -1; + let lastCellWidth = 0; + const cellLabels = []; + + for(const cell of tableCells){ + cell.style.whiteSpace = 'nowrap'; + const hypotenuse = cell.offsetWidth; + const text = cell.innerText; + const height = solveRightTriangle({hypotenuse}); + const idealHeight = Math.min(height, maxCellHeight); + maxWidth = Math.max(hypotenuse, maxWidth); + + cell.style.height = idealHeight + 'px'; + cell.setAttribute('aria-label', text); + const cellLabel = crel('cell-label'); + cellLabel.innerText = text; + cellLabel.setAttribute('label', text); + cellLabel.style.transform = `rotate(${360 - angle}deg)`; + cellLabel.style.maxWidth = maxHeadingWidth + 'px'; + const 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); + } + } + 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}`; + } + return lastCellWidth - textTruncateOffset; +}; \ No newline at end of file