├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── table-saw.js └── demo.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zachleat/table-saw", 3 | "version": "1.0.7", 4 | "description": "A small web component for responsive elements.", 5 | "main": "table-saw.js", 6 | "scripts": { 7 | "start": "npx http-server ." 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/zachleat/table-saw.git" 12 | }, 13 | "license": "MIT", 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/zachleat/table-saw/issues" 19 | }, 20 | "author": { 21 | "name": "Zach Leatherman", 22 | "email": "zachleatherman@gmail.com", 23 | "url": "https://zachleat.com/" 24 | }, 25 | "homepage": "https://github.com/zachleat/table-saw#readme" 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zach Leatherman @zachleat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `table-saw` 2 | 3 | A small structural-only zero-dependency Web Component for responsive `
` elements. Heavily inspired by [Filament Group’s Tablesaw Stack jQuery plugin](https://github.com/filamentgroup/tablesaw). 4 | 5 | ## [Demo](https://zachleat.github.io/table-saw/demo.html) 6 | 7 | ## Examples 8 | 9 | ```html 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 |
41 |
42 | ``` 43 | 44 | * Use `breakpoint` attribute to set the breakpoint (default:`(max-width: 39.9375em)`). 45 | * Use `type="container"` attribute to use container queries instead of viewport-based media queries (default: `type="media"`). 46 | * Use `ratio` attribute to override the small viewport column ratio (default: `1/2`). 47 | * Use `zero-padding` attribute to remove small viewport padding on table cells. 48 | * Use `text-align` attribute to force column text alignment at small viewport. 49 | 50 | ## Installation 51 | 52 | You have a few options (choose one of these): 53 | 54 | 1. Install via [npm](https://www.npmjs.com/package/@zachleat/table-saw): `npm install @zachleat/table-saw` 55 | 1. [Download the source manually from GitHub](https://github.com/zachleat/table-saw/tags) into your project. 56 | 1. Skip this step and use the script directly via a 3rd party CDN (not recommended for production use) 57 | 58 | ### Usage 59 | 60 | Make sure you include the ` 66 | ``` 67 | 68 | ```html 69 | 70 | 71 | ``` 72 | 73 | ```html 74 | 75 | 76 | ``` 77 | 78 | ## Features 79 | 80 | * Supports one or many `` child elements. 81 | * Works with viewport media queries or container queries. 82 | * Uses CSS grid for small viewport alignment. 83 | * Falls back to regular table before or without JavaScript. 84 | * Cuts the mustard on [`CSSStyleSheet.prototype.replaceSync`](https://caniuse.com/mdn-api_cssstylesheet_replacesync) 85 | -------------------------------------------------------------------------------- /table-saw.js: -------------------------------------------------------------------------------- 1 | class Tablesaw extends HTMLElement { 2 | static dupes = {}; 3 | 4 | constructor() { 5 | super(); 6 | 7 | this.autoOffset = 50; 8 | this._needsStylesheet = true; 9 | 10 | this.attrs = { 11 | breakpoint: "breakpoint", 12 | breakpointBackwardsCompat: "media", 13 | type: "type", 14 | ratio: "ratio", 15 | label: "data-tablesaw-label", 16 | zeropad: "zero-padding", 17 | forceTextAlign: "text-align" 18 | }; 19 | 20 | this.defaults = { 21 | breakpoint: '(max-width: 39.9375em)', // same as Filament Group’s Tablesaw 22 | ratio: '1fr 2fr', 23 | }; 24 | 25 | this.classes = { 26 | wrap: "tablesaw-wrap" 27 | } 28 | 29 | this.props = { 30 | ratio: "--table-saw-ratio", 31 | bold: "--table-saw-header-bold", 32 | }; 33 | } 34 | 35 | generateCss(breakpoint, type) { 36 | return ` 37 | table-saw.${this._id} { 38 | display: block; 39 | ${type === "container" ? "container-type: inline-size;" : ""} 40 | } 41 | 42 | @${type} ${breakpoint} { 43 | table-saw.${this._id} thead :is(th, td) { 44 | position: absolute; 45 | height: 1px; 46 | width: 1px; 47 | overflow: hidden; 48 | clip: rect(1px, 1px, 1px, 1px); 49 | } 50 | table-saw.${this._id} :is(tbody, tfoot) tr { 51 | display: block; 52 | } 53 | table-saw.${this._id} :is(tbody, tfoot) :is(th, td):before { 54 | font-weight: var(${this.props.bold}); 55 | content: attr(${this.attrs.label}); 56 | } 57 | table-saw.${this._id} :is(tbody, tfoot) :is(th, td) { 58 | display: grid; 59 | gap: 0 1em; 60 | grid-template-columns: var(${this.props.ratio}, ${this.defaults.ratio}); 61 | } 62 | table-saw.${this._id}[${this.attrs.forceTextAlign}] :is(tbody, tfoot) :is(th, td) { 63 | text-align: ${this.getAttribute(this.attrs.forceTextAlign) || "left"}; 64 | } 65 | table-saw.${this._id}[${this.attrs.zeropad}] :is(tbody, tfoot) :is(th, td) { 66 | padding-left: 0; 67 | padding-right: 0; 68 | } 69 | }`; 70 | } 71 | 72 | connectedCallback() { 73 | // Cut-the-mustard 74 | // https://caniuse.com/mdn-api_cssstylesheet_replacesync 75 | if(!("replaceSync" in CSSStyleSheet.prototype)) { 76 | return; 77 | } 78 | 79 | this.addHeaders(); 80 | this.setRatio(); 81 | 82 | if(!this._needsStylesheet) { 83 | return; 84 | } 85 | 86 | let sheet = new CSSStyleSheet(); 87 | let breakpoint = this.getAttribute(this.attrs.breakpoint) || this.getAttribute(this.attrs.breakpointBackwardsCompat) || this.defaults.breakpoint; 88 | let type = this.getAttribute(this.attrs.type) || "media"; 89 | 90 | this._id = `ts_${type.slice(0, 1)}${breakpoint.replace(/[^a-z0-9]/gi, "_")}`; 91 | this.classList.add(this._id); 92 | 93 | if(!Tablesaw.dupes[this._id]) { 94 | let css = this.generateCss(breakpoint, type); 95 | sheet.replaceSync(css); 96 | 97 | let root = this.getRootNode(); 98 | root.adoptedStyleSheets.push(sheet); 99 | 100 | // only add to global de-dupe if not a shadow root 101 | if(root.host && root !== root.host.shadowRoot) { 102 | Tablesaw.dupes[this._id] = true; 103 | } 104 | } 105 | } 106 | 107 | addHeaders() { 108 | let headerCells = this.querySelectorAll("thead th"); 109 | let labels = Array.from(headerCells).map((cell, index) => { 110 | // Set headers to be bold (if headers are bold) 111 | if(index === 0) { 112 | let styles = window.getComputedStyle(cell); 113 | if(styles) { 114 | // Copy other styles? 115 | let bold = styles.getPropertyValue("font-weight"); 116 | this.setBold(bold); 117 | } 118 | } 119 | 120 | let label = cell.innerText.trim(); 121 | if (label === "") { 122 | label = cell.textContent.trim(); 123 | } 124 | 125 | return label; 126 | }); 127 | 128 | if(labels.length === 0) { 129 | this._needsStylesheet = false; 130 | console.error("No `
` elements found:", this); 131 | return; 132 | } 133 | 134 | let cells = this.querySelectorAll("tbody :is(td, th)"); 135 | for(let cell of cells) { 136 | if(!labels[cell.cellIndex]) { 137 | continue; 138 | } 139 | 140 | cell.setAttribute(this.attrs.label, labels[cell.cellIndex]); 141 | 142 | let nodeCount = 0; 143 | for(let n of cell.childNodes) { 144 | // text or element node 145 | if(n.nodeType === 3 || n.nodeType === 1) { 146 | nodeCount++; 147 | } 148 | } 149 | 150 | // wrap if this cell has child nodes for correct grid alignment 151 | if(nodeCount > 1) { 152 | let wrapper = document.createElement("div"); 153 | wrapper.classList.add(this.classes.wrap); 154 | while(cell.firstChild) { 155 | wrapper.appendChild(cell.firstChild); 156 | } 157 | cell.appendChild(wrapper); 158 | } 159 | } 160 | } 161 | 162 | setBold(bold) { 163 | if(bold || bold === "") { 164 | this.style.setProperty(this.props.bold, bold); 165 | } 166 | } 167 | 168 | setRatio() { 169 | let ratio = this.getAttribute(this.attrs.ratio); 170 | if(ratio) { 171 | let ratioString = ratio.split("/").join("fr ") + "fr"; 172 | this.style.setProperty(this.props.ratio, ratioString); 173 | } 174 | } 175 | } 176 | 177 | if("customElements" in window) { 178 | window.customElements.define("table-saw", Tablesaw); 179 | } 180 | 181 | export { Tablesaw }; 182 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | table-saw Web Component Demo 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

table-saw Web Component Demo

31 |

Source code available at https://github.com/zachleat/table-saw/

32 |
33 |
34 |

Using media queries (viewport based)

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
Movie TitleRankYearRatingGross
Avatar1200983%$2.7B
Titanic2199788%$2.1B
The Avengers3201292%$1.5B
Harry Potter and the Deathly Hallows—Part 24201196%$1.3B
Frozen5201389%$1.2B
Iron Man 36201378%$1.2B
Transformers: Dark of the Moon7201136%$1.1B
The Lord of the Rings: The Return of the King8200395%$1.1B
Skyfall9201292%$1.1B
Transformers: Age of Extinction10201418%$1.0B
119 |
120 | 121 |

Using container queries

122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 |
Movie TitleRankYearRatingGross
Avatar1200983%$2.7B
Titanic2199788%$2.1B Link test
The Avengers3201292%$1.5B
Harry Potter and the Deathly Hallows—Part 24201196%$1.3B
Frozen5201389%$1.2B
Iron Man 36201378%$1.2B
Transformers: Dark of the Moon7201136%$1.1B
The Lord of the Rings: The Return of the King8200395%$1.1B
Skyfall9201292%$1.1B
Transformers: Age of Extinction10201418%$1.0B
207 |
208 |
209 | 210 |

Multiple child tables in one <table-saw>

211 | 212 |

First table

213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 241 | 245 | 249 | 250 | 251 | 252 | 256 | 260 | 264 | 265 | 268 | 269 | 270 | 271 | 275 | 276 | 277 | 278 | 279 | 280 | 281 |
Pricing StrategiesHourly-basedProject-basedValue-basedRetainer-basedTier-based
OverviewPricing per hour.Fixed price per project.Fixed price based on value delivered.Fixed price for a set number of hours.Fixed price packages at different price points.
BenefitsStraightforward and easy to calculate.Ensures budget predictability, and encourages efficiency. 238 | Higher earnings when the impact is significant. Emphasizes 239 | results. 240 | 242 | Ensures a steady and predictable income stream. Builds longterm 243 | business relationships. 244 | 246 | Caters to various client needs and budgets. Simplifies clients’ 247 | decision-making. 248 |
Challenges 253 | May incentivize slower work. Emphasizes hours worked rather than 254 | value of work. 255 | 257 | Unforeseen changes or requests can lead to scope creep, and 258 | threaten profitability. 259 | 261 | Dependent on ability to communicate the value delivered 262 | effectively. 263 | Dependent on client’s trust. 266 | Lack of transparency in terms and pricing may cause confusion. 267 |
Ideal projects or clients 272 | Small projects, or projects subject to change due to timelines 273 | or requirements. 274 | Projects with clear expectations and scope.Clients with clear pain points and business goals.Repeat and ongoing client projects.Client roster with widely varying needs and budgets.
282 |

Second table

283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 311 | 315 | 319 | 320 | 321 | 322 | 326 | 330 | 334 | 335 | 338 | 339 | 340 | 341 | 345 | 346 | 347 | 348 | 349 | 350 | 351 |
Pricing StrategiesHourly-basedProject-basedValue-basedRetainer-basedTier-based
OverviewPricing per hour.Fixed price per project.Fixed price based on value delivered.Fixed price for a set number of hours.Fixed price packages at different price points.
BenefitsStraightforward and easy to calculate.Ensures budget predictability, and encourages efficiency. 308 | Higher earnings when the impact is significant. Emphasizes 309 | results. 310 | 312 | Ensures a steady and predictable income stream. Builds longterm 313 | business relationships. 314 | 316 | Caters to various client needs and budgets. Simplifies clients’ 317 | decision-making. 318 |
Challenges 323 | May incentivize slower work. Emphasizes hours worked rather than 324 | value of work. 325 | 327 | Unforeseen changes or requests can lead to scope creep, and 328 | threaten profitability. 329 | 331 | Dependent on ability to communicate the value delivered 332 | effectively. 333 | Dependent on client’s trust. 336 | Lack of transparency in terms and pricing may cause confusion. 337 |
Ideal projects or clients 342 | Small projects, or projects subject to change due to timelines 343 | or requirements. 344 | Projects with clear expectations and scope.Clients with clear pain points and business goals.Repeat and ongoing client projects.Client roster with widely varying needs and budgets.
352 |
353 | 354 |

Console logs an error when missing `th` elements

355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 371 | 372 | 373 | 374 |
Pricing StrategiesHourly-basedProject-based
Ideal projects or clients 368 | Small projects, or projects subject to change due to timelines 369 | or requirements. 370 | Projects with clear expectations and scope.
375 |
376 | 377 |

Hidden in details

378 |
379 | Expand to show 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 396 | 397 | 398 | 399 |
Pricing StrategiesHourly-basedProject-based
Ideal projects or clients 393 | Small projects, or projects subject to change due to timelines 394 | or requirements. 395 | Projects with clear expectations and scope.
400 |
401 |
402 | 403 |

Shadow root

404 | 431 |
432 |
433 | 434 | 435 | --------------------------------------------------------------------------------