1// Uncompressed script. DokuWikis JS compressor does not support ASI. To be compressed with https://closure-compiler.appspot.com/ (SIMPLE) 2 3window.mobileTables = ((options) => { 4 5 options = options || {} 6 7 // A CSS query selector to find all tables to be transformed. 8 const selector = options.selector || "table" 9 10 // A callback to determine the index column of a table. 11 const parseColumnIndex = options.parseColumnIndex || (node => -1) 12 13 // Special schema values that are not repeated on the mobile table. Instead a cell with colspan = 2 is created. 14 const hideHeadings = options.hideHeadings || [] 15 16 // Holds references to the original <table> elements to undo the transformation. 17 const tableMap = new WeakMap() 18 19 // Holds references to the transformed <th> and <td> elements to undo the transformation. 20 const cellMap = new WeakMap() 21 22 // Indicates that a cell should be treated as part of the index column. 23 const indexColumn = Symbol("index") 24 25 // Indicates that the cell heading should not be shown. The resulting cell will span both columns. 26 const hiddenHeading = Symbol("hidden") 27 28 // Creates an array of header cells that can be cloned for each "row" of the mobile version of the table. 29 const extractSchema = (table, columnIndex) => { 30 const schema = [] 31 32 // Get the schema from the first row. 33 const row = table.querySelector("tr") 34 let i = 0 35 36 for (let cell of row.children) { 37 const colSpan = cell.colSpan 38 let indexSpan = 0 39 40 // Add a cell one or more times to the schema (adjusting for colspan). 41 while (indexSpan < colSpan) { 42 if (i === columnIndex) { 43 schema.push(indexColumn) 44 } else if (hideHeadings.includes(cell.innerText.trim())) { 45 schema.push(hiddenHeading) 46 } else { 47 let td = document.createElement("td") 48 td.innerHTML = cell.innerHTML 49 schema.push(td) 50 } 51 52 i = i + 1 53 indexSpan = indexSpan + 1 54 } 55 } 56 57 return schema 58 } 59 60 const isTextCell = cell => cell.childNodes.length === 1 && cell.lastChild.nodeName === "#text" 61 62 // Move cell contents instead of cloning to keep attached event handlers (footnotes etc.). 63 const moveContent = (oldCell, newCell) => { 64 // Text nodes can just be copied. 65 if (isTextCell(oldCell)) { 66 newCell.innerText = oldCell.innerText 67 return false 68 } 69 70 //newCell.append(...oldCell.childNodes) 71 while (oldCell.firstChild) { 72 newCell.appendChild(oldCell.firstChild) 73 } 74 return true 75 } 76 77 const addRow = tbody => { 78 const tr = document.createElement("tr") 79 tbody.appendChild(tr) 80 return tr 81 } 82 83 // Adds a cell containing content moved from the original table. 84 const addCell = (tr, cell, colSpan) => { 85 const newCell = cell.cloneNode(false) 86 if (colSpan) { 87 newCell.colSpan = 2 88 } 89 90 if (moveContent(cell, newCell)) { 91 cellMap.set(cell, newCell) 92 } 93 tr.appendChild(newCell) 94 95 return newCell 96 } 97 98 const addHeaderCell = (tr, cell) => { 99 const newCell = document.createElement("th") 100 newCell.colSpan = 2 101 // Copy the CSS class for alignement etc. 102 newCell.className = cell.className 103 104 if (moveContent(cell, newCell)) { 105 cellMap.set(cell, newCell) 106 } 107 tr.appendChild(newCell) 108 109 //return newCell 110 } 111 112 const addNameCell = (tr, name) => { 113 let cell 114 115 if (isTextCell(name)) { 116 cell = document.createElement("td") 117 cell.innerText = name.innerText 118 } else { 119 cell = name.cloneNode(true) 120 } 121 122 tr.appendChild(cell) 123 124 //return cell 125 } 126 127 const buildTable = (table, schema) => { 128 const columnIndex = schema.indexOf(indexColumn) 129 130 // Create shallow copies of the table and tbody. 131 const newTable = table.cloneNode(false) 132 const tbody = table.querySelector("tbody").cloneNode(false) 133 newTable.appendChild(tbody) 134 135 // Check for rowspans that need to be skipped. 136 const rowSpans = new Array(schema.length).fill(0) 137 138 let skip = true 139 140 // Iterating all children of the <tbody> is not sufficient as there may be multiple <tr> elements inside <thead>. 141 for (let row of table.querySelectorAll("tr")) { 142 // Skip the first row (header). 143 if (skip) { 144 skip = false 145 continue 146 } 147 148 // A random header row appeared! 149 if (row.children.length === 1) { 150 addCell(addRow(tbody), row.firstElementChild, true) 151 // This resets row spans. 152 rowSpans.fill(0) 153 continue 154 } 155 156 // If there is an index column, create a header row. 157 const header = (columnIndex !== -1) ? addRow(tbody) : null 158 159 // Check for colspans that need to be converted to rowspans. 160 let i = 0 161 let rowSpan = 1 162 163 let colOffset = 0 164 165 for (let name of schema) { 166 if (rowSpans[i] > 0) { 167 rowSpans[i] = rowSpans[i] - 1 168 colOffset = colOffset + 1 169 i = i + 1 170 continue 171 } 172 173 if (row.children[i - colOffset] === undefined) { 174 console.log("mobileTables: Unsupported table layout:") 175 console.log(row) 176 break 177 } 178 179 if (i === columnIndex) { 180 // The index column has already been created above, add the content. 181 addHeaderCell(header, row.children[i - colOffset]) 182 } else { 183 const tr = addRow(tbody) 184 if (name !== hiddenHeading) { 185 addNameCell(tr, name) 186 } 187 188 const colSpan = row.children[i - colOffset].colSpan 189 rowSpans[i] = row.children[i - colOffset].rowSpan - 1 190 191 if (rowSpan === 1) { 192 const newCell = addCell(tr, row.children[i - colOffset], name === hiddenHeading) 193 newCell.rowSpan = colSpan 194 } 195 196 rowSpan = (rowSpan === 1) ? colSpan : rowSpan - 1 197 if (rowSpan > 1) { 198 continue 199 } 200 } 201 202 i = i + 1 203 } 204 } 205 206 return newTable 207 } 208 209 const replaceWithDummy = table => { 210 // Create a dummy element to take the place of the table so we can modify it outside the document tree. 211 const dummy = document.createElement("div") 212 table.replaceWith(dummy) 213 return dummy 214 } 215 216 const transform = tables => { 217 tables = tables || document.querySelectorAll(selector) 218 219 let mutation = false 220 221 for (let table of tables) { 222 if (tableMap.has(table) || table.classList.contains("mobiletable-transformed")) { 223 return 224 } 225 226 const columnIndex = parseColumnIndex(table) 227 228 // Create the mobile table. 229 const dummy = replaceWithDummy(table) 230 const mobile = buildTable(table, extractSchema(table, columnIndex)) 231 232 // Replace the original table and save it for later. 233 tableMap.set(mobile, table) 234 dummy.replaceWith(mobile) 235 236 mobile.classList.add("mobiletable-transformed") 237 238 mutation = true 239 } 240 241 return mutation 242 } 243 244 const undo = tables => { 245 tables = tables || document.querySelectorAll(selector) 246 247 let mutation = false 248 249 for (let table of tables) { 250 const original = tableMap.get(table) 251 252 if (original === undefined) { 253 //console.log("mobileTables: Cannot find original for table:") 254 //console.log(table) 255 continue 256 } 257 258 const dummy = replaceWithDummy(table) 259 260 // Move the cell contents back to the original table. 261 for (let cell of original.querySelectorAll("td, th")) { 262 const transformed = cellMap.get(cell) 263 264 if (transformed !== undefined) { 265 moveContent(transformed, cell) 266 } 267 } 268 269 dummy.replaceWith(original) 270 271 mutation = true 272 } 273 274 return mutation 275 } 276 277 return (isMobile, tables) => isMobile ? transform(tables) : undo(tables) 278 279})({ 280 selector: "div.page div.mobiletable table", 281 parseColumnIndex: node => { 282 const index = parseInt(node.parentElement.parentElement.getAttribute("data-column"), 10) 283 return (isNaN(index) || index < 0) ? -1 : index 284 }, 285 hideHeadings: window.JSINFO["plugin_mobiletable_hideHeadings"] || [] 286}) 287 288 289window.checkMobileTables = () => { 290 const div = document.querySelector("div.mobiletable") 291 292 if (!div) { 293 return 294 } 295 296 const before = window.getComputedStyle(div, ":before") 297 .getPropertyValue("content") 298 .replace(/"|'/g, "") 299 300 if (window.mobileTables(before === "mobile") && window.location.hash) { 301 // Scroll to anchor after transformation. 302 window.location.hash = window.location.hash 303 } 304} 305 306// document.ready 307(cb => ["complete", "interactive"].includes(document.readyState) ? setTimeout(cb, 0) : document.addEventListener("DOMContentLoaded", cb))(() => { 308 309 let resizeTimer 310 311 window.addEventListener("resize", () => { 312 if (resizeTimer) { 313 clearTimeout(resizeTimer) 314 } 315 resizeTimer = setTimeout(window.checkMobileTables, 200) 316 }) 317 318 window.checkMobileTables() 319}) 320