PageSourceSearch

https://www.atlit.org.il/js/tableCards.js?v=1788852215

js atlit.org.il collected 2026-10-02 14:21:33 UTC 15,788 bytes, 421 lines download raw bytes

1/* Global mobile "table -> cards" layer.
2 *
3 * Content editors build tables in the WYSIWYG editor with fixed pixel/percent
4 * widths and 4-8 columns. On a phone those tables push the page sideways. This
5 * script turns such a table into one card per row ("label: value" lines), and
6 * where no header row can be identified it falls back to a horizontal scroll
7 * box so the page itself never overflows.
8 *
9 * The layout is CSS-only (css/tableCards.css, @media max-width:767px); this
10 * file only tags the DOM. Desktop rendering is never touched.
11 *
12 * Safety - the script is a FALLBACK and deliberately refuses to act unless it
13 * is sure nobody else owns the table:
14 *   - it runs one tick after DOMContentLoaded, so every per-schema and
15 *     per-mercaz script has already had its turn;
16 *   - it only ever touches a table that is ACTUALLY WIDER than its container
17 *     at mobile width - a table that already fits is left completely alone;
18 *   - it skips application widgets (carts, modals, tabs, datepickers,
19 *     DataTables, sliders), anything holding form controls, nested tables and
20 *     tables another stylesheet/script already stacks or labels;
21 *   - labels use data-tc-label, not data-label/data-th/data-title, so it can
22 *     never collide with the per-schema card CSS that already uses those.
23 *
24 * Manual override on a single table: data-tablecards="off" | "on"
25 * ("on" only bypasses the overflow test - the safety skips still apply).
26 * Re-scan after injecting content with jQuery/ajax: window.asTableCards.scan()
27 */
28(function () {
29    'use strict';
30
31    var MQ = '(max-width: 767px)';
32    var MAX_LABEL_LEN = 45;   /* longer first-row cells read as data, not a header */
33    var NOT_ECHO = -2;        /* headerEchoCell(): the row is real data */
34    var FLAG = 'asTableCardsDone';
35    var REJECTED = 'asTableCardsSkip';
36
37    /* Widgets that own their tables' markup, row order or visibility. */
38    var SKIP_INSIDE = '.modal, .tab-pane, .accordion_table_wrapper, .swiper-container,' +
39        ' .swiper-wrapper, .slick-slider, .owl-carousel, .ui-datepicker, .fc,' +
40        ' .dataTables_wrapper, #datatable, #cartForm, .keytanaDataForMobile';
41
42    /* Tables that already ship a mobile behaviour of their own. */
43    var SKIP_SELF = '.table_responsive_new, .viowOrderTable, .personal-cart-table,' +
44        ' .dataTable, .tcard, .no-mobile, .no-desktop, .safari-table, .non-safari-table,' +
45        ' .as-tc, .as-tc-scroll';
46
47    /* A cell already carrying one of these is labelled by someone else's CSS. */
48    var SKIP_LABELLED = '[data-label], [data-th], [data-title], .is-table-header-row';
49
50    function closest(el, sel) {
51        if (el.closest) {
52            return el.closest(sel);
53        }
54        var node = el;
55        while (node && node.nodeType === 1) {
56            if (matches(node, sel)) {
57                return node;
58            }
59            node = node.parentElement;
60        }
61        return null;
62    }
63
64    function matches(el, sel) {
65        var fn = el.matches || el.msMatchesSelector || el.webkitMatchesSelector;
66        return fn ? fn.call(el, sel) : false;
67    }
68
69    function text(el) {
70        return (el.textContent || '').replace(/\s+/g, ' ').trim();
71    }
72
73    /* The table is already stacked by another stylesheet if its rows/cells no
74       longer compute to table display at this width - leave it to them. */
75    function alreadyStacked(table) {
76        var row = table.rows[table.rows.length - 1];
77        if (!row) {
78            return true;
79        }
80        if (window.getComputedStyle(row).display !== 'table-row') {
81            return true;
82        }
83        var cell = row.cells[0];
84        return !!cell && window.getComputedStyle(cell).display !== 'table-cell';
85    }
86
87    /* The table itself, or an ancestor, already scrolls horizontally - the
88       overflow is contained and somebody meant it that way. */
89    function inScrollBox(table) {
90        var node = table;
91        for (var i = 0; node && node.nodeType === 1 && i < 4; i++, node = node.parentElement) {
92            var overflow = window.getComputedStyle(node).overflowX;
93            if (overflow === 'auto' || overflow === 'scroll') {
94                return true;
95            }
96        }
97        return false;
98    }
99
100    /* Only tables that genuinely break the layout are worth changing. */
101    function overflows(table) {
102        var host = table.parentElement;
103        if (!host) {
104            return false;
105        }
106        var room = host.clientWidth || document.documentElement.clientWidth;
107        if (room < 1) {
108            return false;
109        }
110        var width = Math.max(table.scrollWidth, Math.ceil(table.getBoundingClientRect().width));
111        return width - room > 4;     /* tolerance: sub-pixel rounding is not overflow */
112    }
113
114    /* Reasons that can never change with the viewport - checked once, then
115       remembered, so the resize handler stays cheap on table-heavy pages. */
116    function isDisqualified(table) {
117        if (table[REJECTED]) {
118            return true;
119        }
120        var no = table.getAttribute('data-tablecards') === 'off' ||
121            matches(table, SKIP_SELF) ||
122            !!closest(table, SKIP_INSIDE) ||
123            /* nested tables, and tables inside a table, are layout scaffolding */
124            table.getElementsByTagName('table').length > 0 ||
125            (table.parentElement && !!closest(table.parentElement, 'table')) ||
126            /* anything interactive belongs to the application, not the editor */
127            !!table.querySelector('input, select, textarea, button, [contenteditable]') ||
128            !!table.querySelector(SKIP_LABELLED) ||
129            table.rows.length < 2;
130
131        if (no) {
132            table[REJECTED] = true;
133        }
134        return no;
135    }
136
137    function isCandidate(table) {
138        if (table[FLAG] || isDisqualified(table)) {
139            return false;
140        }
141        /* width-dependent, so re-evaluated on every pass */
142        if (alreadyStacked(table) || inScrollBox(table)) {
143            return false;
144        }
145        return table.getAttribute('data-tablecards') === 'on' || overflows(table);
146    }
147
148    /* Resolve rowspan/colspan into a row x column grid so every visual column
149       has an entry, and cells spanning down can be found from the rows below. */
150    function buildGrid(rows) {
151        var grid = [];
152        for (var r = 0; r < rows.length; r++) {
153            grid[r] = grid[r] || [];
154            var c = 0;
155            var cells = rows[r].cells;
156            for (var i = 0; i < cells.length; i++) {
157                var cell = cells[i];
158                while (grid[r][c]) {
159                    c++;
160                }
161                var rowSpan = cell.rowSpan || 1;
162                var colSpan = cell.colSpan || 1;
163                for (var rr = 0; rr < rowSpan; rr++) {
164                    for (var cc = 0; cc < colSpan; cc++) {
165                        grid[r + rr] = grid[r + rr] || [];
166                        grid[r + rr][c + cc] = { cell: cell, ownRow: rr === 0 };
167                    }
168                }
169                c += colSpan;
170            }
171        }
172        return grid;
173    }
174
175    function distinctCells(gridRow) {
176        var out = [];
177        for (var c = 0; c < (gridRow || []).length; c++) {
178            var entry = gridRow[c];
179            if (entry && out.indexOf(entry.cell) === -1) {
180                out.push(entry.cell);
181            }
182        }
183        return out;
184    }
185
186    /* Only ~a quarter of editor tables use <th>, so fall back to reading the
187       first multi-column row as the header - but only when it looks like one:
188       every column filled, short plain text, no media, and real data below. */
189    function findHeaderIndex(rows, grid) {
190        var i;
191
192        for (i = 0; i < rows.length; i++) {
193            var cells = distinctCells(grid[i]);
194            if (cells.length < 2) {
195                continue;   /* full-width row above the header = title banner */
196            }
197
198            var allTh = true;
199            var looksLikeLabels = true;
200            for (var j = 0; j < cells.length; j++) {
201                var cell = cells[j];
202                if (cell.tagName !== 'TH') {
203                    allTh = false;
204                }
205                var value = text(cell);
206                if (!value || value.length > MAX_LABEL_LEN ||
207                    cell.querySelector('img, table, ul, ol, iframe, br')) {
208                    looksLikeLabels = false;
209                }
210            }
211
212            if (allTh) {
213                return i;
214            }
215            /* a header made of <td> is only believable with data rows under it */
216            if (looksLikeLabels && rows.length - i >= 3) {
217                return i;
218            }
219            return -1;      /* first real row is data - no header to read */
220        }
221        return -1;
222    }
223
224    function labelsFrom(gridRow) {
225        var labels = [];
226        for (var c = 0; c < (gridRow || []).length; c++) {
227            labels[c] = gridRow[c] ? text(gridRow[c].cell) : '';
228        }
229        return labels;
230    }
231
232    /* A cell with no text and no media adds an empty line to the card. */
233    function isBlank(cell) {
234        return !text(cell) && !cell.querySelector('img, svg, input, a');
235    }
236
237    function allBlank(gridRow) {
238        var cells = distinctCells(gridRow);
239        for (var i = 0; i < cells.length; i++) {
240            if (!isBlank(cells[i])) {
241                return false;
242            }
243        }
244        return true;
245    }
246
247    /* Editor tables often repeat the header row part-way down to start a new
248       section, putting the section name in the first cell and the same column
249       titles in the rest ("נציגי עירייה | מייל | טל | כתובת"). As a card that
250       reads "מייל: מייל", so such a row becomes a section banner showing only
251       the name - or is dropped entirely when every cell just repeats a label. */
252    function headerEchoCell(gridRow, labels) {
253        var cols = 0;
254        var matched = 0;
255        var odd = -1;
256
257        for (var c = 0; c < (gridRow || []).length; c++) {
258            var entry = gridRow[c];
259            if (!entry || !entry.ownRow) {
260                return NOT_ECHO;    /* spanned cell - not a plain header repeat */
261            }
262            cols++;
263            if (labels[c] && text(entry.cell) === labels[c]) {
264                matched++;
265            } else if (odd === -1) {
266                odd = c;
267            } else {
268                return NOT_ECHO;    /* two columns differ - it is real data */
269            }
270        }
271        if (cols < 3 || matched < 2 || matched < cols - 1) {
272            return NOT_ECHO;
273        }
274        return odd;                 /* -1 = pure repeat, >=0 = the section name */
275    }
276
277    /* Keep one cell of a section-banner row, drop the repeated column titles. */
278    function keepOnly(gridRow, col) {
279        var keep = gridRow[col] ? gridRow[col].cell : null;
280        var cells = distinctCells(gridRow);
281        for (var i = 0; i < cells.length; i++) {
282            if (cells[i] !== keep) {
283                cells[i].className += ' as-tc-echo';
284            }
285        }
286    }
287
288    function toScrollBox(table) {
289        table[FLAG] = true;
290        table.className = table.className ? table.className + ' as-tc-scroll' : 'as-tc-scroll';
291    }
292
293    function toCards(table, rows, grid, headerIdx) {
294        var labels = labelsFrom(grid[headerIdx]);
295
296        for (var r = 0; r < rows.length; r++) {
297            var row = rows[r];
298
299            if (r < headerIdx) {
300                /* full-width row above the header: a banner, unless it is the
301                   blank spacer row editors leave between sections */
302                row.className += allBlank(grid[r]) ? ' as-tc-head' : ' as-tc-title';
303                continue;
304            }
305            if (r === headerIdx) {
306                row.className += ' as-tc-head';
307                continue;
308            }
309
310            var echo = headerEchoCell(grid[r], labels);
311            if (echo !== NOT_ECHO) {
312                if (echo < 0) {
313                    row.className += ' as-tc-head';     /* nothing but repeated labels */
314                } else {
315                    row.className += ' as-tc-title';
316                    keepOnly(grid[r], echo);
317                }
318                continue;
319            }
320
321            row.className += ' as-tc-row';
322
323            var seen = [];
324            for (var c = 0; c < grid[r].length; c++) {
325                var entry = grid[r][c];
326                if (!entry || seen.indexOf(entry.cell) !== -1) {
327                    continue;
328                }
329                seen.push(entry.cell);
330
331                if (entry.ownRow) {
332                    entry.cell.setAttribute('data-tc-label', labels[c] || '');
333                    entry.cell.style.order = c;
334                    if (isBlank(entry.cell)) {
335                        entry.cell.className += ' as-tc-empty';
336                    }
337                } else {
338                    /* the cell spans down from an earlier row: clone it into this
339                       card so each card is self-contained. The clone is appended
340                       at the end, so `order` restores the original column order -
341                       it is inert in the desktop table layout, where the clone is
342                       hidden anyway. */
343                    var clone = entry.cell.cloneNode(true);
344                    clone.removeAttribute('rowspan');
345                    clone.setAttribute('data-tc-label', labels[c] || '');
346                    clone.className += ' as-tc-clone';
347                    clone.style.order = c;
348                    if (isBlank(clone)) {
349                        clone.className += ' as-tc-empty';
350                    }
351                    row.appendChild(clone);
352                }
353            }
354        }
355
356        table[FLAG] = true;
357        table.className = table.className ? table.className + ' as-tc' : 'as-tc';
358    }
359
360    function convert(table) {
361        var rows = [];
362        for (var i = 0; i < table.rows.length; i++) {
363            rows.push(table.rows[i]);
364        }
365
366        var grid = buildGrid(rows);
367        var headerIdx = findHeaderIndex(rows, grid);
368
369        if (headerIdx === -1 || headerIdx >= rows.length - 1) {
370            toScrollBox(table);     /* no labels to read - keep it scrollable */
371            return;
372        }
373        toCards(table, rows, grid, headerIdx);
374    }
375
376    function scan() {
377        if (!window.matchMedia || !window.matchMedia(MQ).matches) {
378            return;
379        }
380        var tables = document.getElementsByTagName('table');
381        /* live collection + we append cells: snapshot first */
382        var list = [];
383        for (var i = 0; i < tables.length; i++) {
384            list.push(tables[i]);
385        }
386        for (var j = 0; j < list.length; j++) {
387            if (isCandidate(list[j])) {
388                convert(list[j]);
389            }
390        }
391    }
392
393    var pending = null;
394    function scanSoon() {
395        if (pending) {
396            clearTimeout(pending);
397        }
398        pending = setTimeout(function () {
399            pending = null;
400            scan();
401        }, 150);
402    }
403
404    function start() {
405        /* one tick late, so per-schema / per-mercaz DOMContentLoaded handlers
406           (which may card these tables themselves) run first */
407        setTimeout(scan, 0);
408    }
409
410    if (document.readyState === 'loading') {
411        document.addEventListener('DOMContentLoaded', start);
412    } else {
413        start();
414    }
415    /* late webfonts and images change what overflows */
416    window.addEventListener('load', scanSoon);
417    window.addEventListener('resize', scanSoon);
418    window.addEventListener('orientationchange', scanSoon);
419
420    window.asTableCards = { scan: scan };
421})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.