PageSourceSearch

https://www.afrostylicity.com/wp-content/plugins/shoppable-lookboo…ppable-lookbook-image-hotspots.js?ver=1.9.3

js afrostylicity.com collected 2026-10-02 14:46:18 UTC 27,245 bytes, 585 lines download raw bytes

1(function ($) {
2    "use strict";
3
4    // Breakpoint below which markers open as a bottom-sheet drawer.
5    function isMobile() {
6        return window.matchMedia('(max-width: 768px)').matches;
7    }
8
9    // Per-lookbook "Mobile Display" setting: the full-screen bottom sheet
10    // (default) or the "compact" style, which keeps the desktop box
11    // behaviour (positioned next to its marker, sized down via CSS) instead
12    // of portaling into a sheet. $lb is the ".shopspot" root.
13    function usesSheet($lb) {
14        return isMobile() && ! $lb.hasClass('lookbook-mobile-compact');
15    }
16
17    // The mobile sheet behaves like a toast: no backdrop, no scroll lock —
18    // the page stays usable while the sheet sits fixed at the bottom.
19
20    // Keep a desktop box inside the viewport. Pages no longer clip their
21    // boxes (flipbook leaves are overflow:visible), so a box opened near the
22    // window edge — or on the outer half of a flipbook spread — can poke
23    // off-screen. Measure it and shift it back with a translate; if it is
24    // wider than the viewport, keeping the left edge visible wins.
25    function clampBoxToViewport($box) {
26        if ( ! $box.length ) { return; }
27        var el = $box[0];
28
29        // Suspend the box transition while measuring: it transitions "all"
30        // with a 0.4s delay, so a cleared transform would otherwise keep
31        // reporting the previous clamp's geometry and poison the numbers
32        // (a re-position pass would then see the box as fine, drop the
33        // shift, and let it slide back off-screen).
34        el.style.transition = 'none';
35        el.style.transform = 'none';
36
37        var rect  = el.getBoundingClientRect(),
38            pad   = 10,
39            vw    = document.documentElement.clientWidth,
40            shift = 0;
41        if ( rect.width ) {
42            if ( rect.right > vw - pad ) {
43                shift = vw - pad - rect.right;
44            }
45            if ( rect.left + shift < pad ) {
46                shift = pad - rect.left;
47            }
48        }
49        el.style.transform = shift ? 'translateX(' + shift + 'px)' : '';
50
51        // Restore the stylesheet transition on the next frame, after the
52        // instant (un-animated) shift above has been committed.
53        window.requestAnimationFrame(function () {
54            el.style.transition = '';
55        });
56    }
57
58    // Position every marker's box relative to the photo so it stays on-screen.
59    function positionLookbooks(root) {
60        $(root).find('.shopspot').each(function () {
61            var $lb   = $(this),
62                photo = $lb.find('.lookbook-photo');
63            if ( ! photo.length || ! photo.offset() ) {
64                return;
65            }
66
67            // Never upscale a STANDALONE lookbook past its photo's natural
68            // width — blown-up images go blurry. The ROOT is capped (not just
69            // the img) so the %-positioned markers keep matching the photo;
70            // the stylesheet's auto margins then centre it in the column.
71            // Gallery slides / Product List wrappers size the photo themselves.
72            // The admin's own "Photo Max Width" setting (data-max-width) is a
73            // further, user-chosen ceiling on top of that natural-size cap.
74            if ( photo[0].naturalWidth && ! $lb.closest('.lookbook-gallery, .lookbook-with-list').length ) {
75                var cap = photo[0].naturalWidth,
76                    userMax = parseInt($lb.attr('data-max-width'), 10);
77                if ( userMax > 0 ) {
78                    cap = Math.min(cap, userMax);
79                }
80                $lb.css('max-width', cap + 'px');
81            }
82
83            // In the default "Bottom sheet" style the box becomes a fixed
84            // sheet (styled via CSS), so clear any desktop inline positioning
85            // and skip the math. The "Compact" style keeps the desktop
86            // positioning below, just scaled down by CSS.
87            if ( usesSheet($lb) ) {
88                $lb.find('.lookbook-box').css({ top: '', left: '', right: '', bottom: '', width: '', transform: '' });
89                return;
90            }
91            var isBottomCard = $(this).hasClass('lookbook-layout-4'),
92                photoLeft = photo.offset().left,
93                photoTop = photo.offset().top,
94                limitTop = $(this).data('limit-top'),
95                limitLeft = $(this).data('limit-left'),
96                marginTop = $(this).data('top'),
97                marginBottom = $(this).data('bottom'),
98                marginLeft = $(this).data('left'),
99                marginRight = $(this).data('right');
100
101            $(this).find('.lookbook-marker').each(function () {
102                var halfLeft = $(this).offset().left - photoLeft,
103                    halfTop = $(this).offset().top - photoTop,
104                    $box = $(this).find('.lookbook-box');
105
106                // Prefer the box's real size over the static per-layout limits:
107                // the limits assume a full-width photo and overshoot badly on
108                // small ones (gallery slides, flipbook pages), letting the box
109                // open past the photo edge where containers clip it. The box is
110                // hidden via visibility (not display), so it stays measurable.
111                var boxH = $box.outerHeight() || limitTop,
112                    boxW = $box.outerWidth() || limitLeft;
113
114                // Layout 4 (Bottom card): ignore the marker position and pin
115                // the box inside the photo, centred along its bottom edge.
116                // Use the marker's left/top % against the lookbook's own
117                // client size — same local coordinate space as position:absolute
118                // — so carousel/flipbook transforms can't desync .width() from
119                // .offset() and shove the card to one side.
120                if ( isBottomCard ) {
121                    var cardMargin = 12,
122                        rootEl     = $lb[0],
123                        rootW      = rootEl.clientWidth || photo.width() || 0,
124                        rootH      = rootEl.clientHeight || photo.height() || 0,
125                        cardW      = Math.min( Math.max(0, rootW - cardMargin * 2), 480 ),
126                        markerLeftPct = parseFloat( this.style.left ),
127                        markerTopPct  = parseFloat( this.style.top ),
128                        markerLeftPx  = ( isNaN( markerLeftPct ) ? 0 : markerLeftPct ) / 100 * rootW,
129                        markerTopPx   = ( isNaN( markerTopPct ) ? 0 : markerTopPct ) / 100 * rootH,
130                        markerH       = this.offsetHeight || 0;
131                    $box.removeClass('is-top is-bottom is-left is-right').css({
132                        width:  cardW + 'px',
133                        left:   ( ( rootW - cardW ) / 2 - markerLeftPx ) + 'px',
134                        right:  'auto',
135                        top:    'auto',
136                        bottom: ( markerTopPx + markerH - rootH + cardMargin ) + 'px'
137                    });
138                    return;
139                }
140
141                // Reset any previously computed position classes.
142                $box.removeClass('is-top is-bottom is-left is-right');
143
144                //Top/Bottom position
145                if ( halfTop + boxH + 10 > photo.height() ) {
146                    $box.addClass('is-bottom').css({'bottom':'calc(6% + ' + marginBottom + 'px)', 'top':'auto'});
147                } else {
148                    $box.addClass('is-top').css({'top':'calc(6% - ' + marginTop + 'px)', 'bottom':'auto'});
149                }
150
151                //Right/Left position
152                if ( halfLeft + boxW + 10 > photo.width() ) {
153                    $box.addClass('is-right').css({'right':marginRight, 'left':'auto'});
154                } else {
155                    $box.addClass('is-left').css({'left':marginLeft, 'right':'auto'});
156                }
157
158                clampBoxToViewport($box);
159            });
160        });
161    }
162
163    // ── Mobile sheet portal ─────────────────────────────────────────────────
164    // position:fixed breaks inside transformed ancestors (flipbook 3D pages,
165    // gallery slides): the transform becomes the containing block and the
166    // sheet lands inside the page instead of the viewport. So on mobile the
167    // box is moved to a host under <body> while open. The host mirrors the
168    // lookbook's classes so all sheet CSS still matches.
169
170    // The sheet is fixed (viewport-relative). On a standalone lookbook we pin
171    // it to the photo's bounds so theme column padding doesn't leave the
172    // sheet floating past the image. Inside a carousel / flipbook the photo
173    // (and its ancestors) carry transforms — getBoundingClientRect then
174    // drifts and the sheet lands half off-screen — so those stay full-bleed.
175    function positionSheet($lb, $box) {
176        var vw = document.documentElement.clientWidth || window.innerWidth || 0;
177
178        if ( $lb.closest('.lookbook-gallery').length ) {
179            $box.css({ left: '0', width: '100%', right: 'auto' });
180            return;
181        }
182
183        var photo = $lb.find('.lookbook-photo')[0];
184        if ( ! photo || ! vw ) {
185            $box.css({ left: '0', width: '100%', right: 'auto' });
186            return;
187        }
188        var r = photo.getBoundingClientRect();
189        if ( ! r.width ) {
190            $box.css({ left: '0', width: '100%', right: 'auto' });
191            return;
192        }
193        var left  = Math.max(0, r.left);
194        var right = Math.min(vw, r.right);
195        var width = right - left;
196        // Half-off-screen / degenerate rect → full bleed rather than a stub.
197        if ( width < vw * 0.5 ) {
198            left  = 0;
199            width = vw;
200        }
201        $box.css({ left: left + 'px', width: width + 'px', right: 'auto' });
202    }
203
204    function openSheet($marker) {
205        var $box = $marker.find('.lookbook-box');
206        if ( ! $box.length || $box.data('lbHosted') ) { return; }
207
208        var $lb   = $marker.closest('.shopspot');
209        var $wrap = $marker.closest('.lookbook-markers');
210
211        var $host  = $('<div></div>')
212            .attr('class', 'lookbook-sheet-host ' + ($lb.attr('class') || 'shoppable-lookbook-image-hotspots'))
213            // Analytics resolves the lookbook from its root id; carry it along.
214            .attr('data-lb-id', ($lb.attr('id') || '').replace('shopspot-', ''))
215            .appendTo('body');
216        // Copy class AND style — style holds --lb-bg / --lb-radius / etc. Without
217        // it a custom box colour falls back to white while Auto text colour
218        // (lb-text-light) stays white → unreadable on mobile sheets.
219        var $inner = $('<div></div>')
220            .attr('class', $wrap.attr('class') || 'lookbook-markers')
221            .attr('style', $wrap.attr('style') || null)
222            .appendTo($host);
223        var $ghost = $('<div class="lookbook-marker"></div>').appendTo($inner);
224
225        window.clearTimeout($box.data('lbCloseTimer'));
226        $box.data('lbHosted', { marker: $marker, host: $host, lb: $lb });
227        $ghost.append($box);
228        positionSheet($lb, $box);
229
230        // Let the parked state paint first so the slide-up transition runs.
231        window.requestAnimationFrame(function () { $ghost.addClass('is-active'); });
232    }
233
234    function closeSheet($box, instant) {
235        var info = $box.data('lbHosted');
236        if ( ! info ) { return; }
237
238        $box.parent().removeClass('is-active');
239        $box.removeData('lbHosted');
240
241        var restore = function () {
242            info.marker.append($box);
243            info.host.remove();
244        };
245        if ( instant ) {
246            restore();
247        } else {
248            $box.data('lbCloseTimer', window.setTimeout(restore, 350));
249        }
250    }
251
252    // Instantly restore any hosted sheet (used when resizing up to desktop).
253    function collapseHostedSheets() {
254        $('.lookbook-sheet-host .lookbook-box').each(function () {
255            var $box = $(this), info = $box.data('lbHosted');
256            if ( info && info.marker ) {
257                info.marker.removeClass('is-active').find('.lookbook-grab').attr('aria-expanded', 'false');
258            }
259            closeSheet($box, true);
260        });
261        releaseSheet();
262    }
263
264    function openMarker(grab) {
265        var $grab   = $(grab),
266            $marker = $grab.parent(),
267            $lb     = $marker.closest('.shopspot');
268
269        // "Open All" lookbooks allow many boxes open at once on desktop (and
270        // on the "Compact" mobile style, which behaves like desktop); the
271        // "Bottom sheet" style always keeps one open at a time. force=true so
272        // an accordion's open box yields to the new one.
273        var multiOpen = ! usesSheet($lb) && ( $lb.hasClass('lookbook-trigger-openall') || $lb.hasClass('lookbook-trigger-always') );
274        if ( ! multiOpen ) {
275            closeAllMarkers(true);
276        }
277
278        if ( usesSheet($lb) ) {
279            openSheet($marker);
280        } else if ( ! $lb.hasClass('lookbook-layout-4') ) {
281            // Layout 4 boxes live inside the photo and animate via transform —
282            // the clamp would overwrite that transform and kill the slide-up.
283            clampBoxToViewport($marker.find('.lookbook-box'));
284        }
285
286        $marker.addClass('is-active');
287        $grab.attr('aria-expanded', 'true');
288    }
289
290    function closeMarker(closeBtn) {
291        var $box    = $(closeBtn).closest('.lookbook-box');
292        var hosted  = $box.length ? $box.data('lbHosted') : null;
293        var $marker = hosted ? hosted.marker : $(closeBtn).closest('.lookbook-marker');
294
295        if ( hosted ) {
296            closeSheet($box);
297        } else {
298            // Remove the clamp translate entirely ('' , not 'inherit') so
299            // stylesheet transforms — layout 4's slide-up — keep working.
300            $marker.find('.lookbook-box').css('transform', '');
301        }
302        $marker.removeClass('is-active');
303        $marker.find('.lookbook-grab').attr('aria-expanded', 'false');
304        releaseSheet();
305    }
306
307    function closeAllMarkers(force) {
308        // Hosted mobile sheets first — their boxes live outside the marker.
309        $('.lookbook-sheet-host .lookbook-box').each(function () {
310            closeSheet($(this));
311        });
312        $('.lookbook-marker.is-active').each(function () {
313            // Accordion keeps one box open on desktop (and on the "Compact"
314            // mobile style); it only yields when a new box is about to be
315            // opened (force).
316            if ( ! force && ! usesSheet($(this).closest('.shopspot')) && $(this).closest('.lookbook-trigger-accordion').length ) {
317                return;
318            }
319            $(this).find('.lookbook-box').css('transform', '');
320            $(this).removeClass('is-active').find('.lookbook-grab').attr('aria-expanded', 'false');
321        });
322        releaseSheet();
323    }
324
325    // Auto-open boxes for the "Open All" / "Open First" / "Accordion" triggers.
326    // Runs once per lookbook so boxes the visitor closed stay closed.
327    function applyInitialState(root) {
328        $(root).find('.shopspot').addBack('.shopspot').each(function () {
329            var $lb     = $(this),
330                openAll = $lb.hasClass('lookbook-trigger-openall') || $lb.hasClass('lookbook-trigger-always'),
331                openOne = $lb.hasClass('lookbook-trigger-openfirst') || $lb.hasClass('lookbook-trigger-accordion');
332
333            if ( ! openAll && ! openOne ) { return; }
334            if ( $lb.data('lbAutoOpened') ) { return; }
335            $lb.data('lbAutoOpened', 1);
336
337            if ( usesSheet($lb) ) {
338                // Bottom sheet can only show one box. Open First / Accordion /
339                // Open All all open the first marker as a sheet.
340                var $firstGrab = $lb.find('.lookbook-marker .lookbook-grab').first();
341                if ( $firstGrab.length ) {
342                    openMarker($firstGrab[0]);
343                }
344                return;
345            }
346
347            var $markers = openAll ? $lb.find('.lookbook-marker') : $lb.find('.lookbook-marker').first();
348            $markers.addClass('is-active').find('.lookbook-grab').attr('aria-expanded', 'true');
349        });
350    }
351
352    // Clean up any legacy sheet state when nothing is open.
353    function releaseSheet() {
354        if ( ! $('.lookbook-marker.is-active').length ) {
355            $('body').removeClass('lookbook-sheet-open');
356        }
357    }
358
359    function isActivationKey(e) {
360        return e.key === 'Enter' || e.key === ' ' || e.keyCode === 13 || e.keyCode === 32;
361    }
362
363    // Public initializer so a preview (admin / editor) can (re)position a subtree.
364    window.shoppableLookbookInit = function (root) {
365        positionLookbooks(root || document);
366        applyInitialState(root || document);
367    };
368
369    // (Re)position once each lookbook photo has actually loaded — offsets are
370    // only correct after the image has real dimensions. The listener stays
371    // bound (not just a one-off check) because a responsive img with
372    // srcset/sizes can swap to a different candidate — and fire another
373    // 'load' with a different naturalWidth — after a later viewport resize;
374    // without this the natural-size cap in positionLookbooks() can get
375    // stuck at whatever (possibly smaller) candidate loaded last.
376    function bindImageLoads(root) {
377        $(root).find('.shopspot .lookbook-photo').each(function () {
378            var img = this;
379            if ( img.complete && img.naturalWidth ) {
380                positionLookbooks(img.closest ? img.closest('.shopspot') : document);
381            }
382            $(img).off('load.lookbook').on('load.lookbook', function () {
383                positionLookbooks(img.closest ? img.closest('.shopspot') : document);
384            });
385        });
386    }
387
388    $(document).ready(function () {
389
390        positionLookbooks(document);
391        bindImageLoads(document);
392        applyInitialState(document);
393
394        // After all assets (images/fonts) are loaded.
395        $(window).on('load', function () { positionLookbooks(document); applyInitialState(document); });
396
397        // Reposition on resize / orientation change (debounced).
398        var resizeTimer;
399        $(window).on('resize orientationchange', function () {
400            clearTimeout(resizeTimer);
401            resizeTimer = setTimeout(function () {
402                // A sheet opened on mobile must not survive a switch to desktop.
403                if ( ! isMobile() ) {
404                    collapseHostedSheets();
405                } else {
406                    // Still mobile (e.g. orientation change) — keep any open
407                    // sheet aligned with its photo's new width/position.
408                    $('.lookbook-sheet-host .lookbook-box').each(function () {
409                        var info = $(this).data('lbHosted');
410                        if ( info && info.lb ) { positionSheet(info.lb, $(this)); }
411                    });
412                }
413                positionLookbooks(document);
414                applyInitialState(document);
415            }, 150);
416        });
417
418        // Elementor: re-init when widgets render (editor preview + live page).
419        $(window).on('elementor/frontend/init', function () {
420            if ( window.elementorFrontend && elementorFrontend.hooks ) {
421                elementorFrontend.hooks.addAction('frontend/element_ready/global', function ($scope) {
422                    var node = ( $scope && $scope.get ) ? $scope.get(0) : document;
423                    positionLookbooks(node);
424                    bindImageLoads(node);
425                });
426            }
427        });
428
429        // Fallback for any builder that injects markup via AJAX (WPBakery /
430        // Fusion frontend editors, etc.): observe new lookbooks and re-init.
431        if ( window.MutationObserver ) {
432            var moTimer;
433            var observer = new MutationObserver(function (mutations) {
434                var found = false;
435                mutations.forEach(function (m) {
436                    [].forEach.call(m.addedNodes || [], function (n) {
437                        if ( n.nodeType === 1 && ( (n.classList && n.classList.contains('shoppable-lookbook-image-hotspots')) || (n.querySelector && n.querySelector('.shopspot')) ) ) {
438                            found = true;
439                        }
440                    });
441                });
442                if ( found ) {
443                    clearTimeout(moTimer);
444                    moTimer = setTimeout(function () { positionLookbooks(document); bindImageLoads(document); }, 200);
445                }
446            });
447            observer.observe(document.body, { childList: true, subtree: true });
448        }
449
450        // Hover trigger: open on mouseenter, close when leaving the marker
451        // (desktop only — touch devices keep the tap behaviour).
452        $(document)
453            .on('mouseenter', '.lookbook-trigger-hover .lookbook-grab', function () {
454                if ( ! isMobile() ) { openMarker(this); }
455            })
456            .on('mouseleave', '.lookbook-trigger-hover .lookbook-marker', function () {
457                if ( ! isMobile() ) { closeMarker($(this).find('.lookbook-close')); }
458            });
459
460        // Delegated handlers so dynamically injected lookbooks (previews) also work.
461        $(document)
462            .on('click', '.lookbook-grab', function () { openMarker(this); })
463            .on('keydown', '.lookbook-grab', function (e) {
464                if ( isActivationKey(e) ) { e.preventDefault(); openMarker(this); }
465            })
466            .on('click', '.lookbook-close', function () { closeMarker(this); })
467            .on('keydown', '.lookbook-close', function (e) {
468                if ( isActivationKey(e) ) { e.preventDefault(); closeMarker(this); }
469            })
470            .on('keydown', function (e) {
471                if ( e.key === 'Escape' || e.keyCode === 27 ) {
472                    closeAllMarkers();
473                }
474            })
475            // Variation select: match attributes → update add-to-cart button.
476            .on('change', '.lookbook-variation-select', function () {
477                var $variation = $(this).closest('.lookbook-variation');
478                // Works inside a marker box and inside a Product List card (Pro).
479                var $box       = $(this).closest('.lookbook-box, .lbpl-item');
480                var variations = $variation.data('variations') || [];
481
482                // Collect all selected attribute values.
483                var selected = {};
484                $variation.find('.lookbook-variation-select').each(function () {
485                    selected[$(this).data('attr')] = $(this).val();
486                });
487
488                // All attributes must be chosen.
489                var allPicked = true;
490                $.each(selected, function (k, v) { if (!v) { allPicked = false; return false; } });
491
492                var $btn = $box.find('.lookbook-variation-btn');
493
494                if (!allPicked) {
495                    resetVariationBtn($btn);
496                    return;
497                }
498
499                // Find the first matching in-stock variation.
500                var match = null;
501                for (var i = 0; i < variations.length; i++) {
502                    var v = variations[i];
503                    var ok = true;
504                    for (var key in selected) {
505                        if (!selected.hasOwnProperty(key)) { continue; }
506                        var vAttr = v.attributes[key];
507                        // Empty string in variation data means "any".
508                        if (vAttr !== '' && vAttr !== selected[key]) { ok = false; break; }
509                    }
510                    if (ok && v.in_stock) { match = v; break; }
511                }
512
513                if (match) {
514                    $btn.attr('href', '?add-to-cart=' + match.id + '&quantity=1')
515                        .attr('data-product_id', match.id)
516                        // Set via .data() too — WooCommerce's add-to-cart script
517                        // reads $btn.data('product_id'), and jQuery caches that
518                        // after the first read, ignoring later attr changes.
519                        .data('product_id', match.id)
520                        .addClass('ajax_add_to_cart add_to_cart_button')
521                        .removeAttr('aria-disabled');
522                } else {
523                    resetVariationBtn($btn);
524                }
525            })
526            // Clicking a not-yet-enabled variation button: point at the selects
527            // that still need a choice instead of doing nothing.
528            .on('click', '.lookbook-variation-btn', function (e) {
529                if ( $(this).attr('aria-disabled') !== 'true' ) { return; }
530                e.preventDefault();
531                var $empty = $(this).closest('.lookbook-box, .lbpl-item')
532                    .find('.lookbook-variation-select')
533                    .filter(function () { return ! this.value; });
534                $empty.addClass('lb-attention');
535                setTimeout(function () { $empty.removeClass('lb-attention'); }, 1200);
536            });
537    });
538
539    function resetVariationBtn($btn) {
540        $btn.attr('href', '#')
541            .removeAttr('data-product_id')
542            .removeData('product_id')
543            .removeClass('ajax_add_to_cart add_to_cart_button')
544            .attr('aria-disabled', 'true');
545    }
546
547    // WooCommerce's core AJAX add-to-cart script (enqueued alongside this file)
548    // handles the actual add: it adds/removes the "loading" class itself (styled
549    // as a spinner below) and fires this event on success. Give success an
550    // unmistakable "Added" state instead of WC's default 30%-opacity dim.
551    $(document.body).on('added_to_cart', function (e, fragments, cartHash, $button) {
552        if (!$button || !$button.length || !$button.closest('.lookbook-cart').length) {
553            return;
554        }
555        var i18n = (window.slbihLookbookFront && slbihLookbookFront.i18n) || {};
556        var original = lbOriginalText($button);
557        clearTimeout($button.data('lbRevertTimer'));
558        $button.addClass('lb-cart-added').text(i18n.added || original);
559        var timer = setTimeout(function () {
560            $button.removeClass('lb-cart-added').text(original);
561        }, 2200);
562        $button.data('lbRevertTimer', timer);
563    });
564
565    // Swap the label to "Adding…" the moment the click happens, so the spinner
566    // (::before on .loading, see CSS) isn't sitting next to a static "Add to
567    // cart" the whole time it's working.
568    $(document.body).on('click', '.lookbook-cart .ajax_add_to_cart', function () {
569        var i18n = (window.slbihLookbookFront && slbihLookbookFront.i18n) || {};
570        var $button = $(this);
571        lbOriginalText($button);
572        if (i18n.adding) {
573            $button.text(i18n.adding);
574        }
575    });
576
577    function lbOriginalText($button) {
578        var original = $button.data('lbOriginalText');
579        if (original === undefined) {
580            original = $button.text();
581            $button.data('lbOriginalText', original);
582        }
583        return original;
584    }
585})(jQuery);

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.