Jump to content

MediaWiki:Common.js

From SOJI ELECTRONICS
Revision as of 07:58, 25 May 2026 by Admin (talk | contribs) (Update)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(function () {
    function esc(text) {
        return String(text || "").replace(/[&<>"]/g, function (m) {
            return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[m];
        });
    }

    var ICONS = {
        location: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 010-5 2.5 2.5 0 010 5z"/></svg>',
        phone: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1a9 9 0 00-9 9v7a3 3 0 003 3h3v-8H5v-2a7 7 0 0114 0v2h-4v8h3a3 3 0 003-3v-7a9 9 0 00-9-9z"/></svg>',
        email: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 006 0v-1a10 10 0 10-4 8"/></svg>',
        clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
        facebook: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M22 12a10 10 0 10-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.51 1.49-3.89 3.77-3.89 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.77l-.44 2.89h-2.33v6.99A10 10 0 0022 12z"/></svg>',
        whatsapp: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.5 14.4c-.3-.1-1.7-.8-1.9-.9-.3-.1-.5-.1-.7.1-.2.3-.7.9-.9 1.1-.2.2-.3.2-.6.1-.3-.1-1.2-.4-2.3-1.4-.9-.8-1.4-1.7-1.6-2-.2-.3 0-.5.1-.6.1-.1.3-.3.4-.5.1-.2.2-.3.3-.5.1-.2 0-.4 0-.5 0-.1-.7-1.7-1-2.3-.3-.6-.5-.5-.7-.5h-.6c-.2 0-.5.1-.8.4-.3.3-1 1-1 2.5s1.1 2.9 1.2 3.1c.1.2 2.1 3.2 5.1 4.5.7.3 1.3.5 1.7.6.7.2 1.4.2 1.9.1.6-.1 1.7-.7 2-1.4.2-.7.2-1.3.2-1.4-.1-.1-.3-.2-.5-.3zM12 2a10 10 0 00-8.5 15.3L2 22l4.8-1.4A10 10 0 1012 2z"/></svg>',
        linkedin: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zM8.3 18.3H5.7V9.7h2.6v8.6zM7 8.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm11.3 9.8h-2.6v-4.2c0-1 0-2.3-1.4-2.3s-1.6 1.1-1.6 2.2v4.3h-2.6V9.7h2.5v1.2a2.7 2.7 0 012.5-1.4c2.6 0 3.1 1.7 3.1 4v4.8z"/></svg>',
        youtube: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.5 6.2a3 3 0 00-2.1-2.1C19.5 3.5 12 3.5 12 3.5s-7.5 0-9.4.6A3 3 0 00.5 6.2 31.3 31.3 0 000 12a31.3 31.3 0 00.5 5.8 3 3 0 002.1 2.1c1.9.6 9.4.6 9.4.6s7.5 0 9.4-.6a3 3 0 002.1-2.1A31.3 31.3 0 0024 12a31.3 31.3 0 00-.5-5.8zM9.6 15.6V8.4l6.3 3.6-6.3 3.6z"/></svg>'
    };

    function getIcon(name) {
        return '<span class="soji-footer-icon">' + (ICONS[name] || "") + "</span>";
    }

function renderLink(item, extraClass) {
    var cls = extraClass ? ' class="' + extraClass + '"' : "";
    var href = item.href ? esc(item.href) : "#";
    return '<a' + cls + ' href="' + href + '" target="_blank" rel="noopener noreferrer">' + esc(item.label) + "</a>";
}

    function buildCompany(company) {
        if (!company) return "";
        var contacts = (company.contacts || []).map(function (c) {
            return '<li class="soji-footer-contact">' + getIcon(c.icon) +
                '<span>' + esc(c.text) + "</span></li>";
        }).join("");
        return [
            '<section class="soji-footer-company">',
            '<h2 class="soji-footer-company-name">' + esc(company.name) + "</h2>",
            '<ul class="soji-footer-contact-list">' + contacts + "</ul>",
            "</section>"
        ].join("");
    }

    function buildColumns(columns) {
        return (columns || []).map(function (col) {
            var items = (col.items || []).map(function (item) {
                return "<li>" + renderLink(item, "soji-footer-link") + "</li>";
            }).join("");
            return [
                '<section class="soji-footer-col">',
                "<h2>" + esc(col.title) + "</h2>",
                "<ul>" + items + "</ul>",
                "</section>"
            ].join("");
        }).join("");
    }

    function buildSubscribe(data) {
        var sub = data.subscribe || {};
        var social = (data.social || []).map(function (item) {
    var href = item.href ? esc(item.href) : "#";
    var icon = item.icon
        ? getIcon(item.icon)
        : esc(item.short || item.label.charAt(0));
    return '<a class="soji-footer-social-item" href="' + href + '" target="_blank" rel="noopener noreferrer" aria-label="' + esc(item.label) + '">' + icon + "</a>";
}).join("");

        return [
            '<section class="soji-footer-subscribe-col">',
            "<h2>" + esc(sub.title || "Subscribe") + "</h2>",
            '<form class="soji-footer-subscribe-form" action="' + esc(sub.action || "#") + '" method="post">',
            '<input type="email" name="email" class="soji-footer-subscribe-input" placeholder="' + esc(sub.placeholder || "Email Address *") + '" required>',
            '<button type="submit" class="soji-footer-subscribe-btn">' + esc(sub.buttonLabel || "Send") + "</button>",
            "</form>",
            '<div class="soji-footer-social">' + social + "</div>",
            "</section>"
        ].join("");
    }

    function buildFooter(data) {
        return [
            '<section id="soji-site-footer" class="soji-site-footer">',
            '<div class="soji-footer-shell">',
            '<div class="soji-footer-grid">',
            buildCompany(data.company),
            buildColumns(data.columns),
            buildSubscribe(data),
            "</div>",
            data.copyright ? '<div class="soji-footer-copy">' + esc(data.copyright) + "</div>" : "",
            "</div>",
            "</section>"
        ].join("");
    }

function injectFooter(data) {
    if (document.getElementById("soji-site-footer")) return;
    document.body.insertAdjacentHTML("beforeend", buildFooter(data));
    document.body.classList.add("has-soji-footer");
}


    function init() {
        fetch("/index.php?title=MediaWiki:FooterData.json&action=raw")
            .then(function (r) { return r.text(); })
            .then(function (text) { return JSON.parse(text); })
            .then(injectFooter)
            .catch(function (err) { console.error("Footer load failed:", err); });
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", init);
    } else {
        init();
    }
})();


$(function () {
    if (mw.config.get('wgNamespaceNumber') < 0) return;

    var title = mw.config.get('wgTitle');
    var pageName = mw.config.get('wgPageName').replace(/_/g, ' ');

    // Page Main Page: không cần breadcrumb
    if (pageName === 'Main Page') return;

    // Lấy category trực tiếp của page hoặc của category page hiện tại
    var initialCats = mw.config.get('wgCategories') || [];
    if (!initialCats.length) return;

    var api = new mw.Api();
    var chain = [];
    var seen = {};

    // Đi ngược lên cây qua API
    function walkUp(catName) {
        if (!catName || seen[catName] || catName === 'Main Page') {
            return $.Deferred().resolve();
        }
        seen[catName] = true;
        chain.unshift(catName);

        return api.get({
            action: 'query',
            titles: 'Category:' + catName,
            prop: 'categories',
            cllimit: 1,
            format: 'json',
            formatversion: 2
        }).then(function (data) {
            var pages = (data.query && data.query.pages) || [];
            if (!pages.length || !pages[0].categories) return;
            var parent = pages[0].categories[0].title.replace(/^Category:/, '');
            return walkUp(parent);
        });
    }

    walkUp(initialCats[0]).then(function () {
        var parts = [
            '<a href="' + mw.util.getUrl('Main Page') + '">Main Page</a>'
        ];
        chain.forEach(function (c) {
            parts.push('<a href="' + mw.util.getUrl(c) + '">' + c + '</a>');
        });
        parts.push('<strong>' + title + '</strong>');

        var html = '<nav class="custom-breadcrumb">' + parts.join(' &gt; ') + '</nav>';
        $('#mw-content-text').before(html);
    });
});

$(function () {
    // Tự động thêm target="_blank" cho mọi link tới file .pdf
    $('a[href$=".pdf"], a[href*=".pdf?"], a[href*="/file/"][href*=".pdf"]').each(function () {
        $(this).attr('target', '_blank');
        $(this).attr('rel', 'noopener noreferrer');
        // Thêm icon PDF nhỏ
        if (!$(this).find('.pdf-icon').length) {
            $(this).append(' <span class="pdf-icon" title="Open PDF in new tab">📄</span>');
        }
    });
});

$(function () {
    // Force "View" links to open in new tab
    $('.doc-view a').attr({
        target: '_blank',
        rel: 'noopener noreferrer'
    });

    // Force "Download" links to trigger file download
    $('.doc-download a').each(function () {
        var $a = $(this);
        var href = $a.attr('href');
        // Extract filename from URL
        var match = href.match(/\/([^\/]+\.(?:pdf|zip|doc|docx|xls|xlsx))$/i);
        if (match) {
            $a.attr('download', match[1]);
        } else {
            $a.attr('download', '');
        }
    });
});

/* ============================================
 * Auto-collapse TOC sub-headings on page load
 * Only show H2 (main Heading) by default
 * ============================================ */
$(function () {
    var attempts = 0;
    var maxAttempts = 15;

    function collapseAllTOC() {
        // Find all expanded toggle buttons in TOC
        var $toggles = $('.vector-toc-toggle[aria-expanded="true"]');
        
        if ($toggles.length > 0) {
            $toggles.each(function () {
                // Click toggle to collapse (triggers Vue.js handler)
                this.click();
            });
        } else if (attempts < maxAttempts) {
            // TOC chưa render xong, retry
            attempts++;
            setTimeout(collapseAllTOC, 100);
        }
    }

    collapseAllTOC();
});

/* ============================================
 * Sync chevron arrow with collapsible state
 * ============================================ */
$(function () {
    $('.category-block-toggle span[class*="mw-customtoggle-cb-"]').each(function () {
        var $toggle = $(this);
        var match = $toggle.attr('class').match(/mw-customtoggle-(\S+)/);
        if (!match) return;

        var $target = $('#mw-customcollapsible-' + match[1]);
        if (!$target.length) return;

        function syncArrow() {
            if ($target.hasClass('mw-collapsed')) {
                $toggle.removeClass('is-expanded');
            } else {
                $toggle.addClass('is-expanded');
            }
        }

        // Initial state
        syncArrow();

        // Update on click
        $toggle.on('click', function () {
            setTimeout(syncArrow, 50);
        });
    });
});

/* ============================================
 * Pending Changes Badge for Reviewers
 * Show notification of pending changes count
 * ============================================ */
$(function () {
    // Chỉ show cho user có quyền review (reviewer, sysop, admin)
    var userGroups = mw.config.get('wgUserGroups') || [];
    var hasReviewRights = userGroups.some(function (g) {
        return ['reviewer', 'sysop', 'bureaucrat'].indexOf(g) !== -1;
    });

    if (!hasReviewRights) {
        return;
    }

    // Fetch pending count from API
    function fetchPendingCount() {
        new mw.Api().get({
            action: 'query',
            list: 'oldreviewedpages',
            ornamespace: '0|14',
            orfilterredir: 'nonredirects',
            orlimit: 500,
            format: 'json'
        }).done(function (data) {
            var pages = (data.query && data.query.oldreviewedpages) || [];
            updateBadge(pages.length);
        }).fail(function (err) {
            console.log('Failed to fetch pending changes:', err);
        });
    }

    // Update or create badge in sidebar
    function updateBadge(count) {
        var $badge = $('#pending-reviews-badge');

        if (!$badge.length) {
            // Tạo badge mới — tìm sidebar và prepend
            var badgeHtml =
                '<div id="pending-reviews-badge" style="' +
                'padding: 12px 16px;' +
                'margin: 12px 8px;' +
                'background: #fff3e0;' +
                'border: 1px solid #f57c00;' +
                'border-left: 4px solid #e65100;' +
                'border-radius: 4px;' +
                'cursor: pointer;' +
                'transition: background 0.2s;' +
                '">' +
                '<a href="' + mw.util.getUrl('Special:PendingChanges') + '" style="' +
                'color: #e65100;' +
                'text-decoration: none;' +
                'font-weight: 600;' +
                'font-size: 0.95em;' +
                'display: block;' +
                '">' +
                '⚠️ <span id="pending-count" style="' +
                'display: inline-block;' +
                'background: #e65100;' +
                'color: white;' +
                'padding: 2px 10px;' +
                'border-radius: 12px;' +
                'margin: 0 6px;' +
                'font-weight: 700;' +
                '">0</span>' +
                'pending review' +
                '</a>' +
                '</div>';

            // Try multiple sidebar selectors (Vector 2022 + legacy)
            var $sidebar = $('#mw-panel .vector-main-menu-content')
                .add('#mw-panel-toc-list')
                .add('#mw-panel')
                .add('.vector-main-menu')
                .first();

            if ($sidebar.length) {
                $sidebar.prepend(badgeHtml);
                $badge = $('#pending-reviews-badge');
            } else {
                // Fallback: insert at top of body
                $('body').prepend(
                    '<div style="position:fixed;top:60px;right:20px;z-index:9999;">' +
                    badgeHtml + '</div>'
                );
                $badge = $('#pending-reviews-badge');
            }
        }

        $('#pending-count').text(count);

        if (count === 0) {
            $badge.hide();
        } else {
            $badge.show();
        }
    }

    // Initial fetch
    fetchPendingCount();

    // Refresh every 60 seconds
    setInterval(fetchPendingCount, 60000);

    // Also refresh on focus (when user comes back to tab)
    $(window).on('focus', fetchPendingCount);
});

/* ============================================================
 *  Placeholder auto-hide
 *  Logic: section nào có user content → ẩn placeholder trong section đó
 *         section trống chỉ có placeholder → vẫn hiển thị (làm hint)
 * ============================================================ */
( function ( $ ) {
    'use strict';

    function hidePlaceholdersInFilledSections() {
        var $headings = $( '.mw-parser-output h1, .mw-parser-output h2' );
        if ( !$headings.length ) return;

        $headings.each( function ( i ) {
            var $h = $( this );
            var $nextH = $headings.eq( i + 1 );
            var $sectionEls = $nextH.length ? $h.nextUntil( $nextH ) : $h.nextAll();

            // Đọc text trong section, loại trừ text của placeholder
            var contentText = '';
            $sectionEls.each( function () {
                var $clone = $( this ).clone();
                $clone.find( '.soji-placeholder' ).remove();
                contentText += $clone.text();
            });

            // Nếu section có user content (ngoài placeholder) → ẩn placeholder
            if ( contentText.trim().length > 0 ) {
                $sectionEls.find( '.soji-placeholder' ).hide();
            }
        });

        // Xử lý riêng placeholder trong table cell:
        // Nếu cell có content khác → ẩn placeholder của cell đó
        $( '.soji-placeholder:visible' ).each( function () {
            var $ph = $( this );
            var $cell = $ph.closest( 'td, th' );
            if ( $cell.length ) {
                var $clone = $cell.clone();
                $clone.find( '.soji-placeholder' ).remove();
                if ( $clone.text().trim().length > 0 ) {
                    $ph.hide();
                }
            }
        });
    }

    $( hidePlaceholdersInFilledSections );

})( jQuery );

/* ============================================================
 *  Edit summary helper
 *  Show hint placeholder in edit summary field
 * ============================================================ */
( function ( $, mw ) {
    'use strict';

    $( function () {
        if ( mw.config.get( 'wgAction' ) !== 'edit' && mw.config.get( 'wgAction' ) !== 'submit' ) return;

        var $summary = $( '#wpSummary' );
        if ( !$summary.length ) return;

        // Add placeholder hint
        $summary.attr( 'placeholder',
            'Describe your changes (will appear in Revision History — e.g., "Updated electrical specs", "Fixed typo in safety section")'
        );

        // Add character counter
        var $counter = $( '<div class="fw-summary-counter" style="font-size:0.85em; color:#666; margin-top:4px;"></div>' );
        $summary.after( $counter );

        function updateCounter() {
            var len = $summary.val().length;
            var color = len < 10 ? '#d33' : len < 30 ? '#f80' : '#0a0';
            $counter.html(
                '<span style="color:' + color + '">' + len + '</span> characters ' +
                ( len < 10 ? '(too short — describe what changed)' :
                  len < 30 ? '(acceptable — be more specific)' :
                  '(good — descriptive)' )
            );
        }

        $summary.on( 'input keyup', updateCounter );
        updateCounter();
    } );

} )( jQuery, mediaWiki );

/* ============================================================
 *  Auto Revision History Table
 * ============================================================ */
( function ( $, mw ) {
    'use strict';

    function formatVersion( n ) {
        var major = Math.floor( ( n - 1 ) / 10 ) + 1;
        var minor = ( n - 1 ) % 10;
        return major + '.' + minor;
    }

    function formatDate( isoTimestamp ) {
        var d = new Date( isoTimestamp );
        var dd = ( '0' + d.getDate() ).slice( -2 );
        var mm = ( '0' + ( d.getMonth() + 1 ) ).slice( -2 );
        return dd + '/' + mm + '/' + d.getFullYear();
    }

    function escapeHtml( str ) {
        return ( str || '' )
            .replace( /&/g, '&amp;' )
            .replace( /</g, '&lt;' )
            .replace( />/g, '&gt;' )
            .replace( /"/g, '&quot;' );
    }

    function renderRevisionHistory() {
    var $containers = $( '.revision-history-auto' );
    if ( !$containers.length ) return;

    var pageName = mw.config.get( 'wgPageName' );

    new mw.Api().get( {
        action: 'query',
        prop: 'revisions',
        titles: pageName,
        rvprop: 'timestamp|user|comment',
        rvlimit: 'max',
        rvdir: 'newer',           // oldest first
        formatversion: 2
    } ).done( function ( data ) {
        var page = data.query.pages[ 0 ];
        if ( !page || !page.revisions ) {
            $containers.html( '<em>No revision history available.</em>' );
            return;
        }

        var revisions = page.revisions;

        // Case 1: chỉ có 1 revision = template gen, chưa edit
        if ( revisions.length < 2 ) {
            $containers.html(
                '<em style="color:#888">No edits yet. ' +
                'Edit this page to update the content — the first edit will be recorded as v1.0.</em>'
            );
            return;
        }

        // Case 2: có >= 2 revisions
        // - revision 0 = template creation → SKIP
        // - revision 1 = first edit → v1.0
        // - revision 2 = second edit → v1.1
        // - ...
        var rows = [];

        revisions.forEach( function ( rev, i ) {
            // Skip first revision (template creation)
            if ( i === 0 ) return;

            // i=1 → v1.0, i=2 → v1.1, ...
            var version = formatVersion( i );
            var date    = formatDate( rev.timestamp );
            var author  = escapeHtml( rev.user || 'unknown' );
            var desc    = escapeHtml( rev.comment ) || '<em>(no edit summary)</em>';

            rows.push(
                '<tr><td>' + date + '</td>' +
                '<td><strong>v' + version + '</strong></td>' +
                '<td>' + author + '</td>' +
                '<td>' + desc + '</td></tr>'
            );
        } );

        // Newest first
        rows.reverse();

        var html =
            '<table class="wikitable" style="width:100%">' +
            '<thead><tr>' +
            '<th style="width:15%">Date</th>' +
            '<th style="width:10%">Version</th>' +
            '<th style="width:20%">Author</th>' +
            '<th>Description</th>' +
            '</tr></thead>' +
            '<tbody>' + rows.join( '' ) + '</tbody>' +
            '</table>';

        $containers.html( html );
    } ).fail( function ( err ) {
        $containers.html( '<em style="color:#d33">Failed to load revision history.</em>' );
    } );
}

    $( renderRevisionHistory );

} )( jQuery, mediaWiki );

/* ============================================================
 *  Force edit summary in VisualEditor save dialog
 *  Disable Save button until summary is non-empty (min 5 chars)
 * ============================================================ */
( function ( $, mw ) {
    'use strict';

    var MIN_LENGTH = 5;

    function setupVEEnforcement() {
        // Wait for VE save dialog to appear
        var observer = new MutationObserver( function () {
            var $dialog = $( '.ve-ui-mwSaveDialog:visible' );
            if ( !$dialog.length ) return;
            if ( $dialog.data( 'summary-enforced' ) ) return;

            $dialog.data( 'summary-enforced', true );

            var $summaryInput = $dialog.find( 'textarea[name="wpSummary"], input[name="wpSummary"]' );
            if ( !$summaryInput.length ) {
                $summaryInput = $dialog.find( '.ve-ui-mwSaveDialog-summary textarea, .ve-ui-mwSaveDialog-summary input' );
            }
            if ( !$summaryInput.length ) return;

            var $saveBtn = $dialog.find( '.oo-ui-flaggedElement-primary button, button.oo-ui-flaggedElement-primary' );
            if ( !$saveBtn.length ) {
                $saveBtn = $dialog.find( 'a.oo-ui-flaggedElement-primary' );
            }

            // Add hint message above summary
            if ( !$dialog.find( '.soji-summary-hint' ).length ) {
                $summaryInput.before(
                    '<div class="soji-summary-hint" style="background:#fff3cd; color:#856404; padding:8px 12px; border-left:4px solid #ffc107; margin-bottom:8px; font-size:0.9em; border-radius:3px;">' +
                    '⚠ <strong>Edit summary required.</strong> Describe what you changed (minimum ' + MIN_LENGTH + ' characters). Used for Revision History.' +
                    '</div>'
                );
            }

            // Add counter
            if ( !$dialog.find( '.soji-summary-counter' ).length ) {
                $summaryInput.after(
                    '<div class="soji-summary-counter" style="font-size:0.85em; margin-top:4px;"></div>'
                );
            }

            function updateState() {
                var len = $summaryInput.val().trim().length;
                var $counter = $dialog.find( '.soji-summary-counter' );

                if ( len === 0 ) {
                    $counter.html( '<span style="color:#d33">⚠ Empty — please describe your changes</span>' );
                    disableSaveButton( $saveBtn );
                } else if ( len < MIN_LENGTH ) {
                    $counter.html( '<span style="color:#f80">⚠ Too short (' + len + '/' + MIN_LENGTH + ' chars minimum)</span>' );
                    disableSaveButton( $saveBtn );
                } else {
                    $counter.html( '<span style="color:#0a0">✓ ' + len + ' characters — good</span>' );
                    enableSaveButton( $saveBtn );
                }
            }

            $summaryInput.on( 'input keyup change', updateState );
            updateState();
        } );

        observer.observe( document.body, { childList: true, subtree: true } );
    }

    function disableSaveButton( $btn ) {
        $btn.css( 'opacity', '0.5' ).css( 'pointer-events', 'none' );
        $btn.attr( 'aria-disabled', 'true' );
    }

    function enableSaveButton( $btn ) {
        $btn.css( 'opacity', '' ).css( 'pointer-events', '' );
        $btn.removeAttr( 'aria-disabled' );
    }

    $( setupVEEnforcement );

} )( jQuery, mediaWiki );

/* ============================================================
 *  Edit Draft Auto-Save (localStorage)
 *  Save draft mỗi 30s, restore khi mở lại edit
 *  Backup cho trường hợp stash expire hoặc browser crash
 * ============================================================ */
( function ( $, mw ) {
    'use strict';

    var SAVE_INTERVAL = 30 * 1000;  // 30 seconds
    var DRAFT_TTL    = 30 * 24 * 60 * 60 * 1000;  // 30 days
    var STORAGE_PREFIX = 'mw-draft-';

    function getDraftKey() {
        return STORAGE_PREFIX + mw.config.get( 'wgPageName' );
    }

    function saveDraft( content ) {
        try {
            var data = {
                content: content,
                timestamp: Date.now(),
                page: mw.config.get( 'wgPageName' ),
                user: mw.config.get( 'wgUserName' )
            };
            localStorage.setItem( getDraftKey(), JSON.stringify( data ) );
            return true;
        } catch ( e ) {
            console.warn( 'Failed to save draft:', e );
            return false;
        }
    }

    function loadDraft() {
        try {
            var raw = localStorage.getItem( getDraftKey() );
            if ( !raw ) return null;
            var data = JSON.parse( raw );
            if ( Date.now() - data.timestamp > DRAFT_TTL ) {
                localStorage.removeItem( getDraftKey() );
                return null;
            }
            return data;
        } catch ( e ) {
            return null;
        }
    }

    function clearDraft() {
        try {
            localStorage.removeItem( getDraftKey() );
        } catch ( e ) {}
    }

    function formatAge( ts ) {
        var diff = Date.now() - ts;
        var mins = Math.floor( diff / 60000 );
        if ( mins < 1 ) return 'just now';
        if ( mins < 60 ) return mins + ' min ago';
        var hrs = Math.floor( mins / 60 );
        if ( hrs < 24 ) return hrs + ' hour' + ( hrs > 1 ? 's' : '' ) + ' ago';
        var days = Math.floor( hrs / 24 );
        return days + ' day' + ( days > 1 ? 's' : '' ) + ' ago';
    }

    function setupSourceEditor() {
        var $textarea = $( '#wpTextbox1' );
        if ( !$textarea.length ) return;

        // Restore draft on load
        var draft = loadDraft();
        if ( draft && draft.content !== $textarea.val() ) {
            var age = formatAge( draft.timestamp );
            var $banner = $(
                '<div class="mw-draft-banner" style="background:#fff3cd; border-left:4px solid #ffc107; padding:10px 14px; margin:8px 0; border-radius:3px;">' +
                '<strong>📝 Unsaved draft found</strong> (' + age + ')<br>' +
                '<button type="button" class="mw-draft-restore" style="margin-top:6px; margin-right:8px;">Restore draft</button>' +
                '<button type="button" class="mw-draft-discard" style="margin-top:6px;">Discard draft</button>' +
                '</div>'
            );
            $textarea.before( $banner );

            $banner.find( '.mw-draft-restore' ).on( 'click', function () {
                $textarea.val( draft.content );
                $banner.remove();
            } );
            $banner.find( '.mw-draft-discard' ).on( 'click', function () {
                clearDraft();
                $banner.remove();
            } );
        }

        // Auto-save every 30s
        var saveTimer = setInterval( function () {
            saveDraft( $textarea.val() );
            showSaveIndicator();
        }, SAVE_INTERVAL );

        // Save on every change (throttled)
        var changeTimer;
        $textarea.on( 'input', function () {
            clearTimeout( changeTimer );
            changeTimer = setTimeout( function () {
                saveDraft( $textarea.val() );
            }, 2000 );
        } );

        // Clear draft on successful save
        $( '#editform' ).on( 'submit', function () {
            // Wait a bit, then clear if save successful (page redirected)
            setTimeout( clearDraft, 500 );
        } );

        // Save on tab close
        window.addEventListener( 'beforeunload', function () {
            saveDraft( $textarea.val() );
        } );
    }

    function setupVisualEditor() {
        // VE saves to its own state, but we can backup the wikitext periodically
        mw.hook( 've.activationComplete' ).add( function () {
            var saveTimer = setInterval( function () {
                if ( !mw.libs.ve || !mw.libs.ve.targetLoader ) return;
                try {
                    var target = ve.init.target;
                    if ( target && target.getSurface ) {
                        var surface = target.getSurface();
                        if ( surface ) {
                            var html = surface.getHtml();
                            saveDraft( html );
                            showSaveIndicator();
                        }
                    }
                } catch ( e ) {}
            }, SAVE_INTERVAL );
        } );

        mw.hook( 've.deactivationComplete' ).add( function () {
            // Don't clear automatically — user may want to reopen
        } );

        mw.hook( 've.saveComplete' ).add( function () {
            clearDraft();
        } );
    }

    function showSaveIndicator() {
        var $existing = $( '#mw-draft-indicator' );
        if ( $existing.length ) {
            $existing.show().delay( 2000 ).fadeOut( 500 );
            return;
        }
        var $indicator = $(
            '<div id="mw-draft-indicator" style="position:fixed; bottom:20px; right:20px; background:#0a0; color:white; padding:6px 12px; border-radius:4px; font-size:0.85em; z-index:9999; box-shadow:0 2px 8px rgba(0,0,0,0.2);">' +
            '💾 Draft saved locally' +
            '</div>'
        );
        $( 'body' ).append( $indicator );
        $indicator.delay( 2000 ).fadeOut( 500, function () { $( this ).remove(); } );
    }

    $( function () {
        var action = mw.config.get( 'wgAction' );
        if ( action === 'edit' || action === 'submit' ) {
            setupSourceEditor();
        }
        if ( mw.config.get( 'wgVisualEditor' ) ) {
            setupVisualEditor();
        }
    } );

} )( jQuery, mediaWiki );

/* ============================================================
 *  MultiBoilerplate auto-load on dropdown change
 *  Use URL navigation instead of form submission (more reliable)
 * ============================================================ */
( function ( $, mw ) {
    'use strict';

    $( function () {
        // Only run on edit action
        if ( mw.config.get( 'wgAction' ) !== 'edit' ) return;

        // Find MultiBoilerplate dropdown — try multiple selectors
        var $select = $( 'select[name="boilerplate"], #multiboilerplate-select, #boilerplate-select' );

        if ( !$select.length ) {
            console.log( '[MB-AutoLoad] No boilerplate dropdown found on this page' );
            return;
        }

        console.log( '[MB-AutoLoad] Found dropdown:', $select[ 0 ] );

        // Find Load button to hide it
        var $form = $select.closest( 'form' );
        var $submitBtn = $form.find( 'input[type="submit"], button[type="submit"]' ).filter( function () {
            var text = ( $( this ).val() || $( this ).text() || '' ).toLowerCase();
            return text.indexOf( 'load' ) !== -1 || text.indexOf( 'boilerplate' ) !== -1;
        } );

        $submitBtn.hide();

        // Add loading indicator
        var $indicator = $(
            '<span class="mb-loading-indicator" style="margin-left:8px; color:#0054A6; font-style:italic; display:none;">' +
            '⏳ Loading template...' +
            '</span>'
        );
        $select.after( $indicator );

        // Auto-load on dropdown change
        $select.on( 'change', function () {
            var value = $( this ).val();

            console.log( '[MB-AutoLoad] Selected:', value );

            if ( !value || value === '' || value === '-' ) {
                return;
            }

            // Show loading
            $indicator.show();
            $select.prop( 'disabled', true );

            // Build URL with boilerplate parameter
            var url = new URL( window.location.href );
            url.searchParams.set( 'boilerplate', value );

            console.log( '[MB-AutoLoad] Navigating to:', url.toString() );

            // Small delay for visual feedback
            setTimeout( function () {
                window.location.href = url.toString();
            }, 300 );
        } );
    } );

} )( jQuery, mediaWiki );
/* Auto-convert spaces to dashes in software-card filename hints */
$(function() {
    $('.software-card-no-icon-text code').each(function() {
        var text = $(this).text();
        // Replace spaces with dashes
        $(this).text(text.replace(/ /g, '-'));
    });
});

/* ============================================================
 *  Filename hint formatter
 *  Convert spaces → dashes, strip special chars
 * ============================================================ */
( function ( $ ) {
    'use strict';

    function cleanFilename( text ) {
        return text
            .replace( /&amp;/g, '&' )      // decode HTML entity first
            .replace( /&#38;/g, '&' )
            .replace( / & /g, '-' )         // " & " → "-"
            .replace( /&/g, '' )            // remove remaining &
            .replace( / /g, '-' )           // space → dash
            .replace( /-+/g, '-' );         // multiple dashes → single
    }

    function formatFilenameHints() {
        var selectors = [
            '.cert-card-missing code',
            '.software-card-no-icon-text code',
            '.accessory-card-no-icon-text code',
            '.product-no-image-text code',
            '.faq-card-no-icon-text code'
        ].join( ', ' );

        $( selectors ).each( function () {
            var $code = $( this );
            if ( $code.data( 'filename-formatted' ) ) return;

            var text = $code.text();
            var formatted = cleanFilename( text );

            if ( formatted !== text ) {
                $code.text( formatted );
            }
            $code.data( 'filename-formatted', '1' );
        });
    }

    $( formatFilenameHints );

    if ( window.MutationObserver ) {
        var observer = new MutationObserver( function () {
            formatFilenameHints();
        });
        observer.observe( document.body, { childList: true, subtree: true } );
    }

} )( jQuery );

/* Real-time filter for dynamically loaded sidebar items */
( function ( $ ) {
    'use strict';

    var BLOCKED_PATTERNS = [
        'Product_Change_Notifications',
        'Product Change Notifications',
        'Firmware_Changelog',
        'Firmware Changelog',
        'Promotional_Material',
        'Promotional Material',
        'Certifications',
        'Certification_%26_Approvals',
        'Certification & Approvals'
    ];

    function hideBlocked( context ) {
        var $context = context ? $( context ) : $( document );
        $context.find( '#p-categorytree-portlet a, #mw-panel a' ).each( function () {
            var $link = $( this );
            var href = $link.attr( 'href' ) || '';
            var text = $link.text();

            for ( var i = 0; i < BLOCKED_PATTERNS.length; i++ ) {
                var pattern = BLOCKED_PATTERNS[ i ];
                if ( href.indexOf( pattern ) !== -1 || text.indexOf( pattern.replace( /_/g, ' ' ) ) !== -1 ) {
                    $link.closest( 'li, .CategoryTreeItem' ).addClass( 'sidebar-hidden' );
                    break;
                }
            }
        });
    }

    // Run immediately
    hideBlocked();

    // Observe sidebar for dynamic content
    if ( window.MutationObserver ) {
        var sidebar = document.querySelector( '#p-categorytree-portlet, #mw-panel' );
        if ( sidebar ) {
            new MutationObserver( function ( mutations ) {
                mutations.forEach( function ( m ) {
                    if ( m.addedNodes.length > 0 ) {
                        hideBlocked( m.target );
                    }
                });
            }).observe( sidebar, {
                childList: true,
                subtree: true
            });
        }
    }

})( jQuery );

/* ============================================================
 *  Auto-play video on scroll into view (IntersectionObserver)
 *  Play when visible, pause when scrolled out, loop infinitely
 * ============================================================ */
( function () {
    'use strict';

    function setupScrollVideos() {
        if ( !( 'IntersectionObserver' in window ) ) return;

        var videos = document.querySelectorAll( 'video.scroll-play, .scroll-play video' );
        if ( !videos.length ) return;

        var observer = new IntersectionObserver( function ( entries ) {
            entries.forEach( function ( entry ) {
                var video = entry.target;
                if ( entry.isIntersecting ) {
                    video.play().catch( function () {
                        // Autoplay blocked by browser, mute and try again
                        video.muted = true;
                        video.play();
                    });
                } else {
                    video.pause();
                }
            });
        }, {
            threshold: 0.5  // play when 50% visible
        });

        videos.forEach( function ( video ) {
            video.muted = true;        // required for autoplay in most browsers
            video.loop = true;          // infinite loop
            video.playsInline = true;   // iOS Safari support
            observer.observe( video );
        });
    }

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

} )();

/* =========================
   DOWNLOAD PDF BUTTON
   ========================= */
(function () {
    function addPdfButton() {
    /* Chỉ hiện nút trên page Datasheet hoặc User Guide */
    var pageName = (window.mw && mw.config.get("wgPageName")) || document.title;
    pageName = pageName.replace(/_/g, " ");
    
    var allowedSuffixes = [" Datasheet", " User Guide"];
    var shouldShow = allowedSuffixes.some(function (suffix) {
        return pageName.endsWith(suffix);
    });
    if (!shouldShow) return;
    
    /* Phần code tạo button cũ giữ nguyên bên dưới */
    var heading = document.querySelector(".mw-first-heading") ||
                  document.querySelector("#firstHeading") ||
                  document.querySelector("h1.firstHeading");
    if (!heading || document.getElementById("soji-pdf-btn")) return;

    var btn = document.createElement("button");
    btn.id = "soji-pdf-btn";
    btn.className = "soji-pdf-btn";
    btn.title = "Download this page as PDF";
    btn.innerHTML =
        '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" ' +
        'stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
        'stroke-linejoin="round" style="vertical-align:middle;margin-right:6px">' +
        '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>' +
        '<polyline points="7 10 12 15 17 10"/>' +
        '<line x1="12" y1="15" x2="12" y2="3"/>' +
        '</svg>Download PDF';

    btn.addEventListener("click", function () {
        /* ... click handler giữ nguyên (kèm logic hide Related Documents / Revision History) ... */
        var SECTIONS_TO_HIDE = ["Related Documents", "Revision History"];
        var tagged = [];
        SECTIONS_TO_HIDE.forEach(function (sectionName) {
            var headings = document.querySelectorAll("h2, .mw-heading2");
            for (var i = 0; i < headings.length; i++) {
                var h = headings[i];
                var text = h.textContent.replace(/\[\s*edit.*?\]/gi, "").trim();
                if (text.toLowerCase() === sectionName.toLowerCase()) {
                    var startEl = h.closest("div.mw-heading") || h;
                    var cur = startEl;
                    while (cur) {
                        cur.classList.add("soji-print-hide");
                        tagged.push(cur);
                        cur = cur.nextElementSibling;
                        if (!cur) break;
                        if (cur.matches("h1, h2") ||
                            cur.matches("div.mw-heading2, div.mw-heading1") ||
                            (cur.querySelector && cur.querySelector(":scope > h1, :scope > h2"))) {
                            break;
                        }
                    }
                    break;
                }
            }
        });

        var pn = (window.mw && mw.config.get("wgTitle")) || document.title;
        var originalTitle = document.title;
        document.title = pn + " - SOJI Wiki";
        window.print();
        setTimeout(function () {
            tagged.forEach(function (el) { el.classList.remove("soji-print-hide"); });
            document.title = originalTitle;
        }, 500);
    });

    heading.appendChild(btn);
}


    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", addPdfButton);
    } else {
        addPdfButton();
    }
})();
/* =========================
   YOUTUBE FACADE - improved
   ========================= */
(function () {
    function extractVideoId(src) {
        // Match nhiều format URL
        var patterns = [
            /\/embed\/([a-zA-Z0-9_-]{11})/,     // /embed/VIDEOID
            /[?&]v=([a-zA-Z0-9_-]{11})/,         // ?v=VIDEOID
            /youtu\.be\/([a-zA-Z0-9_-]{11})/     // youtu.be/VIDEOID
        ];
        for (var i = 0; i < patterns.length; i++) {
            var match = src.match(patterns[i]);
            if (match) return match[1];
        }
        return null;
    }

    function initYouTubeFacade() {
        var iframes = document.querySelectorAll(
            'iframe[src*="youtube.com/embed/"], iframe[src*="youtube-nocookie.com/embed/"]'
        );
        iframes.forEach(function (iframe) {
            var videoId = extractVideoId(iframe.src);
            if (!videoId) {
                console.warn('YouTube facade: cannot extract video ID from', iframe.src);
                return;
            }

            var width = iframe.getAttribute("width") || iframe.width || 640;
            var height = iframe.getAttribute("height") || iframe.height || 360;

            var facade = document.createElement("div");
            facade.className = "soji-yt-facade";
            facade.style.maxWidth = width + "px";
            facade.setAttribute("role", "button");
            facade.setAttribute("tabindex", "0");

            facade.innerHTML =
                '<img class="soji-yt-thumb" ' +
                'src="https://i.ytimg.com/vi/' + videoId + '/hqdefault.jpg" ' +
                'alt="Video thumbnail" loading="lazy">' +
                '<button class="soji-yt-play" aria-label="Play">' +
                '<svg viewBox="0 0 68 48" aria-hidden="true">' +
                '<path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="#f00"/>' +
                '<path d="M45 24 27 14v20" fill="#fff"/>' +
                '</svg></button>';

            // Try maxresdefault if available (higher quality)
            var img = facade.querySelector('.soji-yt-thumb');
            var hires = new Image();
            hires.onload = function() {
                if (hires.naturalWidth > 320) img.src = hires.src;
            };
            hires.src = 'https://i.ytimg.com/vi/' + videoId + '/maxresdefault.jpg';

            function playVideo() {
                var realIframe = document.createElement("iframe");
                realIframe.src = "https://www.youtube-nocookie.com/embed/" + videoId + 
                                 "?autoplay=1&rel=0&modestbranding=1";
                realIframe.setAttribute("width", width);
                realIframe.setAttribute("height", height);
                realIframe.setAttribute("frameborder", "0");
                realIframe.setAttribute("allow", "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture");
                realIframe.setAttribute("allowfullscreen", "true");
                realIframe.style.cssText = "width:100%;height:100%;position:absolute;inset:0;border:0;";
                facade.innerHTML = "";
                facade.appendChild(realIframe);
                facade.classList.add("soji-yt-playing");
            }

            facade.addEventListener("click", playVideo);
            facade.addEventListener("keydown", function (e) {
                if (e.key === "Enter" || e.key === " ") {
                    e.preventDefault();
                    playVideo();
                }
            });

            iframe.parentNode.replaceChild(facade, iframe);
        });
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", initYouTubeFacade);
    } else {
        initYouTubeFacade();
    }
})();

/* ============================================================
 *  Floating Buttons: move to body level
 *  Escape mw-body-content stacking context
 * ============================================================ */
(function () {
    function moveFloatingButtonsToBody() {
        var fb = document.getElementById('floating-buttons');
        if (!fb) return;
        
        // Already at body level → skip
        if (fb.parentElement === document.body) return;
        
        document.body.appendChild(fb);
        
        // Force styles
        fb.style.position = 'fixed';
        fb.style.zIndex = '99999';
        fb.style.bottom = '24px';
        fb.style.right = '24px';
    }
    
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', moveFloatingButtonsToBody);
    } else {
        moveFloatingButtonsToBody();
    }
    
    // Re-check sau 1s để chắc chắn (nếu floating-buttons được tạo bằng JS khác sau)
    setTimeout(moveFloatingButtonsToBody, 1000);
})();
SOJI Electronics