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