MediaWiki:Common.js: Difference between revisions
Appearance
Update |
Update |
||
| (25 intermediate revisions by the same user not shown) | |||
| Line 6: | Line 6: | ||
} | } | ||
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 | function renderLink(item, extraClass) { | ||
var | 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) { | var items = (col.items || []).map(function (item) { | ||
return "<li>" + renderLink(item, "soji-footer-link") + "</li>"; | return "<li>" + renderLink(item, "soji-footer-link") + "</li>"; | ||
}).join(""); | }).join(""); | ||
return [ | return [ | ||
'<section class="soji-footer-col">', | '<section class="soji-footer-col">', | ||
| Line 25: | Line 53: | ||
].join(""); | ].join(""); | ||
}).join(""); | }).join(""); | ||
} | |||
var | function buildSubscribe(data) { | ||
var sub = data.subscribe || {}; | |||
var social = (data.social || []).map(function (item) { | 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 [ | return [ | ||
'<section id="soji-site-footer" class="soji-site-footer">', | '<section id="soji-site-footer" class="soji-site-footer">', | ||
'<div class="soji-footer-shell">', | '<div class="soji-footer-shell">', | ||
'<div class="soji-footer-grid">' | '<div class="soji-footer-grid">', | ||
buildCompany(data.company), | |||
buildColumns(data.columns), | |||
buildSubscribe(data), | |||
"</div>", | "</div>", | ||
data.copyright ? '<div class="soji-footer-copy">' + esc(data.copyright) + "</div>" : "", | |||
"</div>", | "</div>", | ||
"</section>" | "</section>" | ||
| Line 63: | Line 92: | ||
} | } | ||
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() { | function init() { | ||
| Line 84: | Line 113: | ||
} | } | ||
})(); | })(); | ||
$(function () { | $(function () { | ||
| Line 422: | Line 452: | ||
$summary.on( 'input keyup', updateCounter ); | $summary.on( 'input keyup', updateCounter ); | ||
updateCounter(); | updateCounter(); | ||
} ); | } ); | ||
| Line 827: | Line 821: | ||
/* ============================================================ | /* ============================================================ | ||
* MultiBoilerplate auto-load on dropdown change | * MultiBoilerplate auto-load on dropdown change | ||
* | * Use URL navigation instead of form submission (more reliable) | ||
* ============================================================ */ | * ============================================================ */ | ||
( function ( $ ) { | ( function ( $, mw ) { | ||
'use strict'; | 'use strict'; | ||
$( function () { | $( function () { | ||
// Find MultiBoilerplate dropdown | // Only run on edit action | ||
var $select = $( | 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 | // Find Load button to hide it | ||
var $form = $select.closest( 'form' ); | var $form = $select.closest( 'form' ); | ||
var $submitBtn = $form.find( | 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(); | $submitBtn.hide(); | ||
| Line 862: | Line 857: | ||
$select.after( $indicator ); | $select.after( $indicator ); | ||
// Auto- | // Auto-load on dropdown change | ||
$select.on( 'change', function () { | $select.on( 'change', function () { | ||
var value = $( this ).val(); | var value = $( this ).val(); | ||
console.log( '[MB-AutoLoad] Selected:', value ); | |||
if ( !value || value === '' || value === '-' ) { | if ( !value || value === '' || value === '-' ) { | ||
return; | return; | ||
} | } | ||
// Show loading | // Show loading | ||
$indicator.show(); | $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 () { | setTimeout( function () { | ||
if ( $ | 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( /&/g, '&' ) // decode HTML entity first | |||
.replace( /&/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 { | } 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 EMBED via JS | |||
Template chỉ output data, JS generate full embed | |||
========================= */ | |||
(function () { | |||
function buildEmbed(container) { | |||
if (container.dataset.initialized === '1') return; | |||
container.dataset.initialized = '1'; | |||
var videoId = container.dataset.videoId; | |||
var width = container.dataset.width || '640'; | |||
var align = container.dataset.align || 'center'; | |||
var caption = container.dataset.caption || ''; | |||
if (!videoId) { | |||
container.innerHTML = '<em style="color:#d33">Error: missing video ID</em>'; | |||
return; | |||
} | |||
// Build container styles | |||
container.style.textAlign = align; | |||
container.style.margin = '12px 0'; | |||
// Thumbnail wrapper | |||
var thumbWrap = document.createElement('div'); | |||
thumbWrap.className = 'soji-yt-thumb-wrap'; | |||
thumbWrap.style.cssText = | |||
'position: relative;' + | |||
'display: inline-block;' + | |||
'width: ' + width + 'px;' + | |||
'max-width: 100%;' + | |||
'aspect-ratio: 16/9;' + | |||
'cursor: pointer;' + | |||
'overflow: hidden;' + | |||
'border-radius: 8px;' + | |||
'background: #000;' + | |||
'box-shadow: 0 4px 12px rgba(0,0,0,0.15);'; | |||
// Thumbnail image | |||
var img = document.createElement('img'); | |||
img.src = 'https://i.ytimg.com/vi/' + videoId + '/hqdefault.jpg'; | |||
img.alt = 'Video thumbnail'; | |||
img.loading = 'lazy'; | |||
img.style.cssText = | |||
'width: 100%;' + | |||
'height: 100%;' + | |||
'object-fit: cover;' + | |||
'display: block;' + | |||
'transition: transform 0.3s, filter 0.2s;'; | |||
// Try maxresdefault for better quality | |||
var hiresTest = new Image(); | |||
hiresTest.onload = function () { | |||
if (hiresTest.naturalWidth > 480) { | |||
img.src = 'https://i.ytimg.com/vi/' + videoId + '/maxresdefault.jpg'; | |||
} | |||
}; | |||
hiresTest.src = 'https://i.ytimg.com/vi/' + videoId + '/maxresdefault.jpg'; | |||
// Play button overlay | |||
var overlay = document.createElement('div'); | |||
overlay.className = 'soji-yt-play-overlay'; | |||
overlay.style.cssText = | |||
'position: absolute;' + | |||
'top: 50%;' + | |||
'left: 50%;' + | |||
'transform: translate(-50%, -50%);' + | |||
'width: 72px;' + | |||
'height: 50px;' + | |||
'background: rgba(0, 0, 0, 0.7);' + | |||
'border-radius: 14px;' + | |||
'display: flex;' + | |||
'align-items: center;' + | |||
'justify-content: center;' + | |||
'transition: background 0.2s, transform 0.2s;' + | |||
'pointer-events: none;'; | |||
overlay.innerHTML = | |||
'<svg width="36" height="26" viewBox="0 0 36 26" fill="white">' + | |||
'<polygon points="14,7 14,19 25,13"/>' + | |||
'</svg>'; | |||
thumbWrap.appendChild(img); | |||
thumbWrap.appendChild(overlay); | |||
// Hover effect | |||
thumbWrap.addEventListener('mouseenter', function () { | |||
overlay.style.background = '#f00'; | |||
overlay.style.transform = 'translate(-50%, -50%) scale(1.1)'; | |||
img.style.filter = 'brightness(0.85)'; | |||
}); | |||
thumbWrap.addEventListener('mouseleave', function () { | |||
overlay.style.background = 'rgba(0, 0, 0, 0.7)'; | |||
overlay.style.transform = 'translate(-50%, -50%) scale(1)'; | |||
img.style.filter = ''; | |||
}); | |||
// Click → load iframe | |||
thumbWrap.addEventListener('click', function () { | |||
var iframe = document.createElement('iframe'); | |||
iframe.src = 'https://www.youtube-nocookie.com/embed/' + videoId + | |||
'?autoplay=1&rel=0&modestbranding=1&playsinline=1'; | |||
iframe.style.cssText = | |||
'width: 100%;' + | |||
'height: 100%;' + | |||
'border: 0;' + | |||
'position: absolute;' + | |||
'inset: 0;'; | |||
iframe.setAttribute('frameborder', '0'); | |||
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'); | |||
iframe.setAttribute('allowfullscreen', 'true'); | |||
thumbWrap.innerHTML = ''; | |||
thumbWrap.appendChild(iframe); | |||
thumbWrap.style.cursor = 'default'; | |||
}); | |||
container.appendChild(thumbWrap); | |||
// Caption | |||
if (caption) { | |||
var captionDiv = document.createElement('div'); | |||
captionDiv.style.cssText = | |||
'margin-top: 8px;' + | |||
'font-style: italic;' + | |||
'color: #666;' + | |||
'font-size: 0.9em;'; | |||
captionDiv.textContent = caption; | |||
container.appendChild(captionDiv); | |||
} | |||
} | |||
function initYouTubeEmbeds() { | |||
document.querySelectorAll('.soji-yt-embed').forEach(buildEmbed); | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', initYouTubeEmbeds); | |||
} else { | |||
initYouTubeEmbeds(); | |||
} | |||
})(); | |||
/* ============================================================ | |||
* 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); | |||
})(); | |||
Latest revision as of 08:28, 25 May 2026
(function () {
function esc(text) {
return String(text || "").replace(/[&<>"]/g, function (m) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[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(' > ') + '</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, '&' )
.replace( /</g, '<' )
.replace( />/g, '>' )
.replace( /"/g, '"' );
}
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( /&/g, '&' ) // decode HTML entity first
.replace( /&/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 EMBED via JS
Template chỉ output data, JS generate full embed
========================= */
(function () {
function buildEmbed(container) {
if (container.dataset.initialized === '1') return;
container.dataset.initialized = '1';
var videoId = container.dataset.videoId;
var width = container.dataset.width || '640';
var align = container.dataset.align || 'center';
var caption = container.dataset.caption || '';
if (!videoId) {
container.innerHTML = '<em style="color:#d33">Error: missing video ID</em>';
return;
}
// Build container styles
container.style.textAlign = align;
container.style.margin = '12px 0';
// Thumbnail wrapper
var thumbWrap = document.createElement('div');
thumbWrap.className = 'soji-yt-thumb-wrap';
thumbWrap.style.cssText =
'position: relative;' +
'display: inline-block;' +
'width: ' + width + 'px;' +
'max-width: 100%;' +
'aspect-ratio: 16/9;' +
'cursor: pointer;' +
'overflow: hidden;' +
'border-radius: 8px;' +
'background: #000;' +
'box-shadow: 0 4px 12px rgba(0,0,0,0.15);';
// Thumbnail image
var img = document.createElement('img');
img.src = 'https://i.ytimg.com/vi/' + videoId + '/hqdefault.jpg';
img.alt = 'Video thumbnail';
img.loading = 'lazy';
img.style.cssText =
'width: 100%;' +
'height: 100%;' +
'object-fit: cover;' +
'display: block;' +
'transition: transform 0.3s, filter 0.2s;';
// Try maxresdefault for better quality
var hiresTest = new Image();
hiresTest.onload = function () {
if (hiresTest.naturalWidth > 480) {
img.src = 'https://i.ytimg.com/vi/' + videoId + '/maxresdefault.jpg';
}
};
hiresTest.src = 'https://i.ytimg.com/vi/' + videoId + '/maxresdefault.jpg';
// Play button overlay
var overlay = document.createElement('div');
overlay.className = 'soji-yt-play-overlay';
overlay.style.cssText =
'position: absolute;' +
'top: 50%;' +
'left: 50%;' +
'transform: translate(-50%, -50%);' +
'width: 72px;' +
'height: 50px;' +
'background: rgba(0, 0, 0, 0.7);' +
'border-radius: 14px;' +
'display: flex;' +
'align-items: center;' +
'justify-content: center;' +
'transition: background 0.2s, transform 0.2s;' +
'pointer-events: none;';
overlay.innerHTML =
'<svg width="36" height="26" viewBox="0 0 36 26" fill="white">' +
'<polygon points="14,7 14,19 25,13"/>' +
'</svg>';
thumbWrap.appendChild(img);
thumbWrap.appendChild(overlay);
// Hover effect
thumbWrap.addEventListener('mouseenter', function () {
overlay.style.background = '#f00';
overlay.style.transform = 'translate(-50%, -50%) scale(1.1)';
img.style.filter = 'brightness(0.85)';
});
thumbWrap.addEventListener('mouseleave', function () {
overlay.style.background = 'rgba(0, 0, 0, 0.7)';
overlay.style.transform = 'translate(-50%, -50%) scale(1)';
img.style.filter = '';
});
// Click → load iframe
thumbWrap.addEventListener('click', function () {
var iframe = document.createElement('iframe');
iframe.src = 'https://www.youtube-nocookie.com/embed/' + videoId +
'?autoplay=1&rel=0&modestbranding=1&playsinline=1';
iframe.style.cssText =
'width: 100%;' +
'height: 100%;' +
'border: 0;' +
'position: absolute;' +
'inset: 0;';
iframe.setAttribute('frameborder', '0');
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
iframe.setAttribute('allowfullscreen', 'true');
thumbWrap.innerHTML = '';
thumbWrap.appendChild(iframe);
thumbWrap.style.cursor = 'default';
});
container.appendChild(thumbWrap);
// Caption
if (caption) {
var captionDiv = document.createElement('div');
captionDiv.style.cssText =
'margin-top: 8px;' +
'font-style: italic;' +
'color: #666;' +
'font-size: 0.9em;';
captionDiv.textContent = caption;
container.appendChild(captionDiv);
}
}
function initYouTubeEmbeds() {
document.querySelectorAll('.soji-yt-embed').forEach(buildEmbed);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initYouTubeEmbeds);
} else {
initYouTubeEmbeds();
}
})();
/* ============================================================
* 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);
})();