User:Monster Domosed/common.js: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
Add support for HeroIcon for tooltips?
No edit summary
 
(235 intermediate revisions by the same user not shown)
Line 1: Line 1:
/**
* Rotatable Model Viewer — with 2D / 3D toggle
* All <img> elements created in JS to bypass MediaWiki sanitizer.
*/
(function () {
(function () {
     'use strict';
     'use strict';


     function makeImg(src, alt, className) {
     const urlCache = new Map();
        var i = new Image();
 
         i.src = src;
    function getSize(element) {
        i.alt = alt || '';
         const match = element.className.match(/model_size_(\d+)/);
        i.draggable = false;
         return match ? parseInt(match[1], 10) : 500;
        if (className) i.className = className;
         return i;
     }
     }


     function boot(el) {
     function getFileUrl(fileName) {
         if (el.dataset.rmReady) return;
         if (urlCache.has(fileName)) {
        el.dataset.rmReady = '1';
             return urlCache.get(fileName);
 
        /* ---- config ---- */
        var imageUrl    = el.dataset.image        || '',
            image2dUrl  = el.dataset.imageFlat    || '',
            icon2dUrl  = el.dataset.iconFlat    || '',
            icon3dUrl  = el.dataset.iconStereo  || '',
            iconRotUrl  = el.dataset.iconRotate  || '',
            fw          = +el.dataset.frameWidth  || 300,
            fh          = +el.dataset.frameHeight || 400,
            count      = +el.dataset.frameCount  || 24,
            startFrame  = +el.dataset.startFrame  || 0,
            sensitivity = +el.dataset.sensitivity || 5;
 
        /* ---- 1:1 square for 3D strip ---- */
        var sq = Math.min(fw, fh);
 
        var hint = el.querySelector('.rotatable-model-hint'),
             load = el.querySelector('.rotatable-model-loading'),
            wrap = el.closest('.rotatable-model-wrapper');
 
        if (!imageUrl) {
            if (load) load.textContent = 'Error: no image URL';
            return;
         }
         }


         /* ---- state ---- */
         const apiUrl =
        var cur    = startFrame,
            mw.config.get('wgScriptPath') +
             is2D  = false,
            '/api.php?action=query' +
             dragOK = true,
            '&titles=File:' + encodeURIComponent(fileName) +
             ready  = false;
             '&prop=imageinfo' +
             '&iiprop=url' +
             '&format=json';


         /* ============================================================
         const promise = fetch(apiUrl)
          SQUARE VIEWPORT
            .then(function (response) {
          ============================================================ */
                if (!response.ok) {
        var viewport = document.createElement('div');
                    throw new Error('API request failed');
        viewport.className = 'rm-square-viewport';
                }
        viewport.style.position = 'absolute';
                return response.json();
        viewport.style.width    = sq + 'px';
            })
        viewport.style.height  = sq + 'px';
            .then(function (data) {
        viewport.style.left    = ((fw - sq) / 2) + 'px';
                const page = Object.values(data.query.pages)[0];
        viewport.style.top      = ((fh - sq) / 2) + 'px';
                return page && page.imageinfo ? page.imageinfo[0].url : null;
        viewport.style.overflow = 'hidden';
            })
            .catch(function () {
                return null;
            });


         el.insertBefore(viewport, el.firstChild);
         urlCache.set(fileName, promise);
        return promise;
    }
    function buildAnimationControls(wrap, container, viewer, defaultAnimation, autoplay) {
        const bar = document.createElement('div');
        bar.className = 'model-viewer-controls';


         /* ============================================================
         const playButton = document.createElement('button');
          3D SPRITE STRIP — each frame is sq × sq
        playButton.className = 'model-anim-button';
          ============================================================ */
         playButton.type = 'button';
         var img = makeImg(imageUrl, '', 'rm-strip');


         img.style.position = 'absolute';
         const select = document.createElement('select');
        img.style.height  = sq + 'px';
         select.className = 'model-anim-select';
        img.style.width    = (sq * count) + 'px';
        img.style.top      = '0';
         img.style.left    = '0';


         img.addEventListener('load', function () {
         const slider = document.createElement('input');
            ready = true;
        slider.className = 'model-anim-slider';
            if (load) load.style.display = 'none';
         slider.type = 'range';
            show(startFrame);
         slider.min = '0';
         });
        slider.max = '1000';
         img.addEventListener('error', function () {
        slider.value = '0';
            if (load) { load.textContent = 'Failed to load image'; load.style.color = '#d33'; }
         slider.step = '1';
         });


         viewport.appendChild(img);
         let paused = !autoplay;
        let rafId = null;
        let duration = 0;


         if (img.complete && img.naturalWidth > 0) {
         function updateSlider() {
            ready = true;
            if (duration > 0) {
            if (load) load.style.display = 'none';
                slider.value = String(Math.round((viewer.currentTime / duration) * 1000));
             show(startFrame);
             }
            rafId = requestAnimationFrame(updateSlider);
         }
         }


         /* ============================================================
         function startLoop() {
          2D STATIC IMAGE — renders at full fw × fh
             cancelAnimationFrame(rafId);
          ============================================================ */
             rafId = requestAnimationFrame(updateSlider);
        var staticImg = null;
        if (image2dUrl) {
             staticImg = makeImg(image2dUrl, '', 'rm-static');
             el.insertBefore(staticImg, el.firstChild);
         }
         }


         /* ============================================================
         function setPlaying(playing) {
          MODE TOGGLE — single button, swaps icon on click
             paused = !playing;
          ============================================================ */
             playButton.textContent = playing ? '❚❚' : '';
        var toggleBtn = null,
             if (playing) {
            iconImg2d = null,
                viewer.play();
            iconImg3d = null;
             } else {
 
                 viewer.pause();
        if (image2dUrl) {
             toggleBtn = document.createElement('span');
             toggleBtn.className = 'rm-mode-toggle';
             toggleBtn.setAttribute('role', 'button');
            toggleBtn.setAttribute('tabindex', '0');
             toggleBtn.title = 'Switch to 2D';
 
            if (icon2dUrl) {
                 iconImg2d = makeImg(icon2dUrl, '2D', 'rm-mode-icon rm-icon-2d');
             }
             }
            if (icon3dUrl) {
                iconImg3d = makeImg(icon3dUrl, '3D', 'rm-mode-icon rm-icon-3d');
                iconImg3d.style.display = 'none';
            }
            if (iconImg2d) toggleBtn.appendChild(iconImg2d);
            if (iconImg3d) toggleBtn.appendChild(iconImg3d);
            if (!icon2dUrl && !icon3dUrl) {
                toggleBtn.textContent = '2D';
            }
            el.appendChild(toggleBtn);
         }
         }


         /* ============================================================
         playButton.addEventListener('click', function () {
          ROTATION ICON  (bottom-right)
             setPlaying(paused);
          ============================================================ */
        });
        if (iconRotUrl) {
            var rotDiv = document.createElement('div');
            rotDiv.className = 'rm-rotate-icon';
            rotDiv.appendChild(makeImg(iconRotUrl, 'Drag to rotate', ''));
            el.appendChild(rotDiv);
        }
 
        /* ============================================================
          FRAME DISPLAY
          ============================================================ */
        function show(idx) {
             cur = ((idx % count) + count) % count;
            img.style.left = -(cur * sq) + 'px';
        }


         function nudge(d) {
         slider.addEventListener('input', function () {
             if (!is2D && ready) {
             if (duration > 0) {
                 show(cur + d);
                 viewer.currentTime = (parseFloat(slider.value) / 1000) * duration;
                hideHint();
             }
             }
         }
         });


         function reset() { show(startFrame); }
         viewer.addEventListener('load', function () {
            const animations = viewer.availableAnimations || [];


        function hideHint() {
            if (animations.length) {
            if (hint) hint.classList.add('rm-hidden');
                animations.forEach(function (animName) {
        }
                    const option = document.createElement('option');
 
                    option.value = animName;
        /* ============================================================
                    option.textContent = animName;
          2D ↔ 3D SWITCHING
                    select.appendChild(option);
          ============================================================ */
                 });
        function setMode(mode) {
            is2D = (mode === '2d');
 
            if (is2D) {
                el.classList.add('rm-is-2d');
                if (wrap) wrap.classList.add('rm-is-2d');
                dragOK = false;
                viewport.style.display = 'none';
            } else {
                el.classList.remove('rm-is-2d');
                if (wrap) wrap.classList.remove('rm-is-2d');
                 dragOK = true;
                viewport.style.display = '';
            }
 
            if (iconImg2d && iconImg3d) {
                iconImg2d.style.display = is2D ? 'none' : '';
                iconImg3d.style.display = is2D ? '' : 'none';
            }


            if (toggleBtn) {
                 if (defaultAnimation && animations.indexOf(defaultAnimation) !== -1) {
                toggleBtn.title = is2D ? 'Switch to 3D' : 'Switch to 2D';
                     select.value = defaultAnimation;
                 if (!icon2dUrl && !icon3dUrl) {
                     toggleBtn.textContent = is2D ? '3D' : '2D';
                 }
                 }
            }
        }
        if (toggleBtn) {
            toggleBtn.addEventListener('click', function () {
                setMode(is2D ? '3d' : '2d');
            });
        }


        /* ============================================================
                viewer.animationName = select.value;
          DRAG  (mouse + touch)
                duration = viewer.duration || 0;
          ============================================================ */
        function startDrag(x0) {
            if (!dragOK || !ready) return;
            var acc = 0;
            hideHint();


            function move(cx) {
                select.addEventListener('change', function () {
                var dx = cx - x0;
                    viewer.animationName = select.value;
                x0 = cx;
                    duration = viewer.duration || 0;
                 acc += dx;
                    viewer.currentTime = 0;
                    if (paused) {
                        setPlaying(true);
                    }
                 });


                 while (acc >= sensitivity) {
                 if (paused) {
                     acc -= sensitivity;
                     setPlaying(false);
                    show(cur + 1);
                 }
                 }
                while (acc <= -sensitivity) {
                    acc += sensitivity;
                    show(cur - 1);
                }
            }
            function onMM(e) { move(e.clientX); e.preventDefault(); }
            function onTM(e) {
                if (e.touches.length === 1) { move(e.touches[0].clientX); e.preventDefault(); }
            }
            function stop() {
                el.classList.remove('rm-dragging');
                document.removeEventListener('mousemove',  onMM);
                document.removeEventListener('mouseup',    stop);
                document.removeEventListener('touchmove',  onTM);
                document.removeEventListener('touchend',    stop);
                document.removeEventListener('touchcancel', stop);
            }
            el.classList.add('rm-dragging');
            document.addEventListener('mousemove',  onMM);
            document.addEventListener('mouseup',    stop);
            document.addEventListener('touchmove',  onTM, { passive: false });
            document.addEventListener('touchend',    stop);
            document.addEventListener('touchcancel', stop);
        }


        el.addEventListener('mousedown', function (e) {
                startLoop();
            if (e.target.closest('.rm-mode-toggle')) return;
                bar.classList.add('model-viewer-controls--visible');
            if (e.button === 0 && dragOK) { startDrag(e.clientX); e.preventDefault(); }
        });
        el.addEventListener('touchstart', function (e) {
            if (e.target.closest('.rm-mode-toggle')) return;
            if (e.touches.length === 1 && dragOK) {
                startDrag(e.touches[0].clientX); e.preventDefault();
             }
             }
        }, { passive: false });
        /* ---- wheel ---- */
        el.addEventListener('wheel', function (e) {
            if (!dragOK || !ready) return;
            var d = e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0;
            if (d) { nudge(d); e.preventDefault(); }
        }, { passive: false });
        /* ---- keyboard ---- */
        el.setAttribute('tabindex', '0');
        el.addEventListener('keydown', function (e) {
            if (is2D || !ready) return;
            if (e.key === 'ArrowLeft')  { nudge(-1); e.preventDefault(); }
            if (e.key === 'ArrowRight') { nudge( 1); e.preventDefault(); }
            if (e.key === 'Home')      { reset();  e.preventDefault(); }
         });
         });


         /* ---- nav buttons ---- */
         bar.appendChild(playButton);
        if (wrap) {
        bar.appendChild(select);
            var bL = wrap.querySelector('.rm-btn-left'),
        bar.appendChild(slider);
                bR = wrap.querySelector('.rm-btn-right'),
        container.appendChild(bar);
                bO = wrap.querySelector('.rm-btn-reset');
            if (bL) bL.addEventListener('click', function () { nudge(-1); });
            if (bR) bR.addEventListener('click', function () { nudge( 1); });
            if (bO) bO.addEventListener('click', reset);
        }
 
        /* ---- initial frame ---- */
        show(startFrame);
     }
     }


    /* ---- scan & init ---- */
    function initAll(root) {
        var els = (root || document).querySelectorAll('.rotatable-model');
        for (var i = 0; i < els.length; i++) boot(els[i]);
    }


    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () { initAll(); });
    } else {
        initAll();
    }


     if (typeof mw !== 'undefined' && mw.hook) {
     function loadModel(preview) {
        mw.hook('wikipage.content').add(function ($c) { initAll($c[0]); });
         if (preview.dataset.loaded) {
    }
            return;
}());
 
/**
* Custom Item & Hero Tooltips — replaces Extension:Popups for .item-link-wrapper and .hero-link-wrapper links
*/
( function () {
    'use strict';
 
    var tooltipCache = {};
    var $tooltip = null;
    var currentTarget = null;
    var showTimer = null;
    var hideTimer = null;
 
    var SHOW_DELAY = 250;
    var HIDE_DELAY = 300;
    var EDGE_MARGIN = 10; // margin from viewport edges
 
    /* ─── Create tooltip DOM element ─── */
    function getTooltip() {
         if ( !$tooltip ) {
            $tooltip = $( '<div>' )
                .addClass( 'custom-item-tooltip' )
                .hide()
                .appendTo( document.body )
                .on( 'mouseenter', function () {
                    clearTimeout( hideTimer );
                } )
                .on( 'mouseleave', function () {
                    scheduleHide();
                } );
         }
         }
        return $tooltip;
    }


    /* ─── Positioning ─── */
         preview.dataset.loaded = '1';
    function positionTooltip( anchor ) {
         var tt = getTooltip();
        var rect = anchor.getBoundingClientRect();
        var scrollTop = window.scrollY || document.documentElement.scrollTop;
        var scrollLeft = window.scrollX || document.documentElement.scrollLeft;
        var vpW = window.innerWidth;
        var vpH = window.innerHeight;


         // Render offscreen to measure actual dimensions
         const fileName = preview.dataset.model;
         tt.css( { top: -9999, left: -9999, visibility: 'hidden' } ).show();
         const displayName = preview.dataset.name || fileName;
         var ttW = tt.outerWidth();
         const defaultAnimation = preview.dataset.animation || '';
         var ttH = tt.outerHeight();
         const autoplay = preview.dataset.autoplay === 'true';
         tt.css( 'visibility', '' );
         const size = getSize(preview);


         // ─── Vertical positioning ───
         getFileUrl(fileName + '.glb')
        var top;
            .then(function (url) {
        var spaceBelow = vpH - rect.bottom - 8;
                if (!url) {
        var spaceAbove = rect.top - 8;
                    return;
                }


        if ( spaceBelow >= ttH + EDGE_MARGIN ) {
                const wrap = document.createElement('div');
            // Fits below the anchor
                 wrap.className = 'model-viewer-wrap';
            top = rect.bottom + scrollTop + 8;
        } else if ( spaceAbove >= ttH + EDGE_MARGIN ) {
            // Fits above the anchor
            top = rect.top + scrollTop - ttH - 8;
        } else {
            // Doesn't fit above or below — pin to the edge with more space
            if ( spaceBelow >= spaceAbove ) {
                 top = scrollTop + vpH - ttH - EDGE_MARGIN;
            } else {
                top = scrollTop + EDGE_MARGIN;
            }
        }


        // ─── Horizontal positioning ───
                const container = document.createElement('div');
        var left = rect.left + scrollLeft;
                container.className = 'model-viewer-container';
                container.style.width = size + 'px';
                container.style.height = size + 'px';


        // Overflows right edge
                const viewer = document.createElement('model-viewer');
        if ( left + ttW > scrollLeft + vpW - EDGE_MARGIN ) {
                viewer.src = url;
            left = scrollLeft + vpW - ttW - EDGE_MARGIN;
                viewer.setAttribute('camera-controls', '');
        }
                viewer.setAttribute('touch-action', 'pan-y');
                if (preview.dataset.autoplay === 'true') {
                    viewer.setAttribute('autoplay', '');
                }


        // Overflows left edge
                container.appendChild(viewer);
        if ( left < scrollLeft + EDGE_MARGIN ) {
                wrap.appendChild(container);
            left = scrollLeft + EDGE_MARGIN;
        }


        tt.css( { top: top, left: left } );
                buildAnimationControls(wrap, container, viewer, defaultAnimation, autoplay);
    }


    /* ─── Show tooltip for item ─── */
                const name = document.createElement('div');
    function showItemTooltip( anchor, itemName ) {
                name.className = 'model-name';
        clearTimeout( hideTimer );
                name.textContent = displayName;
        clearTimeout( showTimer );


        showTimer = setTimeout( function () {
                wrap.appendChild(name);
            var tt = getTooltip();
            currentTarget = anchor;


            // Already cached — show immediately
                 preview.replaceWith(wrap);
            if ( tooltipCache[ 'item_' + itemName ] ) {
             });
                tt.html( tooltipCache[ 'item_' + itemName ] ).show();
                positionTooltip( anchor );
                return;
            }
 
            // Loading indicator
            tt.html( '<div class="custom-item-tooltip__loading">Loading…</div>' ).show();
            positionTooltip( anchor );
 
            // Fetch rendered template
            new mw.Api().post( {
                action: 'parse',
                text: '{{Infobox item/auto|default=true|' + itemName + '}}',
                title: mw.config.get( 'wgPageName' ),
                prop: 'text',
                disablelimitreport: true,
                disableeditsection: true,
                wrapoutputclass: '',
                format: 'json'
            } ).then( function ( data ) {
                var html = data.parse.text[ '*' ];
                tooltipCache[ 'item_' + itemName ] = html;
 
                // Only show if the mouse is still on this element
                if ( currentTarget === anchor ) {
                    tt.html( html ).show();
                    positionTooltip( anchor );
                 }
            } ).catch( function () {
                if ( currentTarget === anchor ) {
                    tt.hide();
                }
             } );
 
        }, SHOW_DELAY );
     }
     }


    /* ─── Show tooltip for hero ─── */
     function initPreview(preview) {
     function showHeroTooltip( anchor, heroKey ) {
         if (preview.dataset.initialized) {
         clearTimeout( hideTimer );
            return;
         clearTimeout( showTimer );
         }


         showTimer = setTimeout( function () {
         preview.dataset.initialized = '1';
            var tt = getTooltip();
            currentTarget = anchor;


            // Already cached — show immediately
        const button = document.createElement('button');
            if ( tooltipCache[ 'hero_' + heroKey ] ) {
        button.className = 'model-load-button';
                tt.html( tooltipCache[ 'hero_' + heroKey ] ).show();
        button.type = 'button';
                positionTooltip( anchor );
        button.addEventListener('click', function () {
                return;
            loadModel(preview);
            }
        });
 
            // Loading indicator
            tt.html( '<div class="custom-item-tooltip__loading">Loading…</div>' ).show();
            positionTooltip( anchor );


            // Fetch rendered hero infobox
        preview.appendChild(button);
            new mw.Api().post( {
                action: 'parse',
                text: '{{Infobox hero\n| key = ' + heroKey + '\n}}',
                title: mw.config.get( 'wgPageName' ),
                prop: 'text',
                disablelimitreport: true,
                disableeditsection: true,
                wrapoutputclass: '',
                format: 'json'
            } ).then( function ( data ) {
                var html = data.parse.text[ '*' ];
                tooltipCache[ 'hero_' + heroKey ] = html;
 
                // Only show if the mouse is still on this element
                if ( currentTarget === anchor ) {
                    tt.html( html ).show();
                    positionTooltip( anchor );
                }
            } ).catch( function () {
                if ( currentTarget === anchor ) {
                    tt.hide();
                }
            } );
 
        }, SHOW_DELAY );
     }
     }


    /* ─── Hide tooltip ─── */
     function scan() {
     function scheduleHide() {
         document
         clearTimeout( showTimer );
            .querySelectorAll('.model-placeholder')
        hideTimer = setTimeout( function () {
             .forEach(initPreview);
             if ( $tooltip ) {
                $tooltip.hide();
            }
            currentTarget = null;
        }, HIDE_DELAY );
     }
     }


     /* ─── Initialization ─── */
     let scanTimer = null;
    mw.hook( 'wikipage.content' ).add( function ( $content ) {
        // Handle item tooltips
        $content.find( '.item-link-wrapper' ).each( function () {
            var $wrapper = $( this );
            var itemName = $wrapper.attr( 'data-item-name' );
            if ( !itemName ) return;


            // All links inside the wrapper, not just the first one
    function debouncedScan() {
            var $links = $wrapper.find( 'a[href]' );
        clearTimeout(scanTimer);
            if ( !$links.length ) return;
        scanTimer = setTimeout(scan, 100);
 
    }
            $links.each( function () {
                var $link = $( this );


                // Remove native title from each link
    scan();
                $link.removeAttr( 'title' );


                $link.on( 'mouseenter.itemTooltip', function () {
    const observer = new MutationObserver(debouncedScan);
                    showItemTooltip( this, itemName );
    observer.observe(document.body, {
                } ).on( 'mouseleave.itemTooltip', function () {
        childList: true,
                    scheduleHide();
        subtree: true
                } );
    });
            } );
        } );


        // Handle hero tooltips
})();
        $content.find( '.hero-link-wrapper' ).each( function () {
            var $wrapper = $( this );
            var heroKey = $wrapper.attr( 'data-hero-name' );
            if ( !heroKey ) return;


            // All links inside the wrapper
            var $links = $wrapper.find( 'a[href]' );
            if ( !$links.length ) return;


            $links.each( function () {
                var $link = $( this );


                // Remove native title from each link
                $link.removeAttr( 'title' );


                $link.on( 'mouseenter.heroTooltip', function () {
                    showHeroTooltip( this, heroKey );
                } ).on( 'mouseleave.heroTooltip', function () {
                    scheduleHide();
                } );
            } );
        } );
    } );


} )();
const personal = document.querySelector(".vector-menu-content-list");
if (personal){
const test2 = document.createElement("li");
const test3 = document.createElement("li");
const test4 = document.createElement("li");
test2.innerHTML = `<a href="/User:Monster_Domosed/common.js">JS</a>`;
test3.innerHTML = `<a href="/User:Monster_Domosed/common.css">CSS</a>`;
test4.innerHTML = `<a href="/User:Monster_Domosed/Sandbox">SB</a>`;
personal.prepend(test2, test3, test4);
}

Latest revision as of 00:56, 15 September 2026

(function () {
    'use strict';

    const urlCache = new Map();

    function getSize(element) {
        const match = element.className.match(/model_size_(\d+)/);
        return match ? parseInt(match[1], 10) : 500;
    }

    function getFileUrl(fileName) {
        if (urlCache.has(fileName)) {
            return urlCache.get(fileName);
        }

        const apiUrl =
            mw.config.get('wgScriptPath') +
            '/api.php?action=query' +
            '&titles=File:' + encodeURIComponent(fileName) +
            '&prop=imageinfo' +
            '&iiprop=url' +
            '&format=json';

        const promise = fetch(apiUrl)
            .then(function (response) {
                if (!response.ok) {
                    throw new Error('API request failed');
                }
                return response.json();
            })
            .then(function (data) {
                const page = Object.values(data.query.pages)[0];
                return page && page.imageinfo ? page.imageinfo[0].url : null;
            })
            .catch(function () {
                return null;
            });

        urlCache.set(fileName, promise);
        return promise;
    }
    function buildAnimationControls(wrap, container, viewer, defaultAnimation, autoplay) {
        const bar = document.createElement('div');
        bar.className = 'model-viewer-controls';

        const playButton = document.createElement('button');
        playButton.className = 'model-anim-button';
        playButton.type = 'button';

        const select = document.createElement('select');
        select.className = 'model-anim-select';

        const slider = document.createElement('input');
        slider.className = 'model-anim-slider';
        slider.type = 'range';
        slider.min = '0';
        slider.max = '1000';
        slider.value = '0';
        slider.step = '1';

        let paused = !autoplay;
        let rafId = null;
        let duration = 0;

        function updateSlider() {
            if (duration > 0) {
                slider.value = String(Math.round((viewer.currentTime / duration) * 1000));
            }
            rafId = requestAnimationFrame(updateSlider);
        }

        function startLoop() {
            cancelAnimationFrame(rafId);
            rafId = requestAnimationFrame(updateSlider);
        }

        function setPlaying(playing) {
            paused = !playing;
            playButton.textContent = playing ? '❚❚' : '▶';
            if (playing) {
                viewer.play();
            } else {
                viewer.pause();
            }
        }

        playButton.addEventListener('click', function () {
            setPlaying(paused);
        });

        slider.addEventListener('input', function () {
            if (duration > 0) {
                viewer.currentTime = (parseFloat(slider.value) / 1000) * duration;
            }
        });

        viewer.addEventListener('load', function () {
            const animations = viewer.availableAnimations || [];

            if (animations.length) {
                animations.forEach(function (animName) {
                    const option = document.createElement('option');
                    option.value = animName;
                    option.textContent = animName;
                    select.appendChild(option);
                });

                if (defaultAnimation && animations.indexOf(defaultAnimation) !== -1) {
                    select.value = defaultAnimation;
                }

                viewer.animationName = select.value;
                duration = viewer.duration || 0;

                select.addEventListener('change', function () {
                    viewer.animationName = select.value;
                    duration = viewer.duration || 0;
                    viewer.currentTime = 0;
                    if (paused) {
                        setPlaying(true);
                    }
                });

                if (paused) {
                    setPlaying(false);
                }

                startLoop();
                bar.classList.add('model-viewer-controls--visible');
            }
        });

        bar.appendChild(playButton);
        bar.appendChild(select);
        bar.appendChild(slider);
        container.appendChild(bar);
    }



    function loadModel(preview) {
        if (preview.dataset.loaded) {
            return;
        }

        preview.dataset.loaded = '1';

        const fileName = preview.dataset.model;
        const displayName = preview.dataset.name || fileName;
        const defaultAnimation = preview.dataset.animation || '';
        const autoplay = preview.dataset.autoplay === 'true';
        const size = getSize(preview);

        getFileUrl(fileName + '.glb')
            .then(function (url) {
                if (!url) {
                    return;
                }

                const wrap = document.createElement('div');
                wrap.className = 'model-viewer-wrap';

                const container = document.createElement('div');
                container.className = 'model-viewer-container';
                container.style.width = size + 'px';
                container.style.height = size + 'px';

                const viewer = document.createElement('model-viewer');
                viewer.src = url;
                viewer.setAttribute('camera-controls', '');
                viewer.setAttribute('touch-action', 'pan-y');
                if (preview.dataset.autoplay === 'true') {
                    viewer.setAttribute('autoplay', '');
                }

                container.appendChild(viewer);
                wrap.appendChild(container);

                buildAnimationControls(wrap, container, viewer, defaultAnimation, autoplay);

                const name = document.createElement('div');
                name.className = 'model-name';
                name.textContent = displayName;

                wrap.appendChild(name);

                preview.replaceWith(wrap);
            });
    }

    function initPreview(preview) {
        if (preview.dataset.initialized) {
            return;
        }

        preview.dataset.initialized = '1';

        const button = document.createElement('button');
        button.className = 'model-load-button';
        button.type = 'button';
        button.addEventListener('click', function () {
            loadModel(preview);
        });

        preview.appendChild(button);
    }

    function scan() {
        document
            .querySelectorAll('.model-placeholder')
            .forEach(initPreview);
    }

    let scanTimer = null;

    function debouncedScan() {
        clearTimeout(scanTimer);
        scanTimer = setTimeout(scan, 100);
    }

    scan();

    const observer = new MutationObserver(debouncedScan);
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();





const personal = document.querySelector(".vector-menu-content-list");
if (personal){
	const test2 = document.createElement("li");
	const test3 = document.createElement("li");
	const test4 = document.createElement("li");
	test2.innerHTML = `<a href="/User:Monster_Domosed/common.js">JS</a>`;
	test3.innerHTML = `<a href="/User:Monster_Domosed/common.css">CSS</a>`;
	test4.innerHTML = `<a href="/User:Monster_Domosed/Sandbox">SB</a>`;
	personal.prepend(test2, test3, test4);
}