|
|
| Line 1: |
Line 1: |
| (function () {
| |
| 'use strict';
| |
|
| |
| const urlCache = new Map();
| |
|
| |
| function getSize(element) {
| |
| const className = typeof element.className === 'string'
| |
| ? element.className
| |
| : '';
| |
|
| |
| const match = className.match(/model_size_(\d+)/);
| |
|
| |
| return match ? parseInt(match[1], 10) : 500;
| |
| }
| |
|
| |
| function getFileUrl(fileName) {
| |
| if (urlCache.has(fileName)) {
| |
| return urlCache.get(fileName);
| |
| }
| |
|
| |
| const apiUrl =
| |
| mw.config.get('wgScriptPath') +
| |
| '/api.php?action=query' +
| |
| '&titles=File:' + encodeURIComponent(fileName) +
| |
| '&prop=imageinfo' +
| |
| '&iiprop=url' +
| |
| '&format=json';
| |
|
| |
| const promise = fetch(apiUrl)
| |
| .then(function (response) {
| |
| if (!response.ok) {
| |
| throw new Error('API request failed');
| |
| }
| |
|
| |
| return response.json();
| |
| })
| |
| .then(function (data) {
| |
| const pages = data.query && data.query.pages;
| |
|
| |
| if (!pages) {
| |
| return null;
| |
| }
| |
|
| |
| const page = Object.values(pages)[0];
| |
|
| |
| if (!page || !page.imageinfo || !page.imageinfo[0]) {
| |
| return null;
| |
| }
| |
|
| |
| return page.imageinfo[0].url;
| |
| })
| |
| .catch(function () {
| |
| return null;
| |
| });
| |
|
| |
| urlCache.set(fileName, promise);
| |
|
| |
| return promise;
| |
| }
| |
|
| |
| function loadModel(preview) {
| |
| if (preview.dataset.loaded) {
| |
| return;
| |
| }
| |
|
| |
| preview.dataset.loaded = '1';
| |
|
| |
| const fileName = preview.dataset.model;
| |
|
| |
| if (!fileName) {
| |
| return;
| |
| }
| |
|
| |
| const displayName = preview.dataset.name || fileName;
| |
| const size = getSize(preview);
| |
|
| |
| getFileUrl(fileName + '.glb')
| |
| .then(function (url) {
| |
| if (!url || !preview.isConnected) {
| |
| return;
| |
| }
| |
|
| |
| const wrap = document.createElement('div');
| |
| wrap.className = 'model-viewer-wrap';
| |
|
| |
| const container = document.createElement('div');
| |
| container.className = 'model-viewer-container';
| |
| container.style.width = size + 'px';
| |
| container.style.height = size + 'px';
| |
|
| |
| const viewer = document.createElement('model-viewer');
| |
|
| |
| viewer.src = url;
| |
| viewer.setAttribute('camera-controls', '');
| |
| viewer.setAttribute('touch-action', 'pan-y');
| |
|
| |
| container.appendChild(viewer);
| |
| wrap.appendChild(container);
| |
|
| |
| const name = document.createElement('div');
| |
| name.className = 'model-name';
| |
| name.textContent = displayName;
| |
|
| |
| wrap.appendChild(name);
| |
|
| |
| preview.replaceWith(wrap);
| |
| });
| |
| }
| |
|
| |
| function initPreview(preview) {
| |
| if (
| |
| !preview ||
| |
| preview.dataset.initialized
| |
| ) {
| |
| return;
| |
| }
| |
|
| |
| preview.dataset.initialized = '1';
| |
|
| |
| const button = document.createElement('button');
| |
|
| |
| button.className = 'model-load-button';
| |
| button.type = 'button';
| |
|
| |
| button.addEventListener('click', function () {
| |
| loadModel(preview);
| |
| });
| |
|
| |
| preview.appendChild(button);
| |
| }
| |
|
| |
| /**
| |
| * Builds the interactive .model-placeholder markup (preview <img>, hint,
| |
| * name) from the data attributes carried by a no-JS .model-fallback block.
| |
| * The preview image URL is fetched from the wiki API asynchronously.
| |
| */
| |
| function buildPlaceholder(data) {
| |
| const model = data.model;
| |
| const name = data.name || model;
| |
| const altImage = data.altImage || model;
| |
|
| |
| const sizeMatch = String(data.modelsize || '').match(/(\d+)/);
| |
| const size = sizeMatch ? parseInt(sizeMatch[1], 10) : 300;
| |
|
| |
| const el = document.createElement('div');
| |
| el.className = 'model-placeholder model_size_' + size + 'px';
| |
| el.dataset.model = model;
| |
| el.dataset.name = name;
| |
|
| |
| const preview = document.createElement('div');
| |
| preview.className = 'model-preview';
| |
|
| |
| const img = document.createElement('img');
| |
| img.className = 'model-preview-img';
| |
| img.alt = name;
| |
|
| |
| preview.appendChild(img);
| |
| el.appendChild(preview);
| |
|
| |
| const hint = document.createElement('div');
| |
| hint.className = 'model-hint';
| |
| hint.textContent = 'Click to view';
| |
| preview.appendChild(hint);
| |
|
| |
| const nameEl = document.createElement('div');
| |
| nameEl.className = 'model-name';
| |
| nameEl.textContent = name;
| |
| el.appendChild(nameEl);
| |
|
| |
| if (altImage) {
| |
| getFileUrl(altImage + '.png').then(function (url) {
| |
| if (url && img.isConnected) {
| |
| img.src = url;
| |
| }
| |
| });
| |
| }
| |
|
| |
| return el;
| |
| }
| |
|
| |
| /**
| |
| * Upgrades a no-JS .model-fallback block into the interactive placeholder,
| |
| * preserving every data attribute the template supplied.
| |
| */
| |
| function hydrateFallback(fallback) {
| |
| if (
| |
| !fallback ||
| |
| fallback.dataset.hydrated ||
| |
| fallback.dataset.model === undefined
| |
| ) {
| |
| return;
| |
| }
| |
|
| |
| fallback.dataset.hydrated = '1';
| |
|
| |
| const placeholder = buildPlaceholder({
| |
| model: fallback.dataset.model,
| |
| name: fallback.dataset.name,
| |
| altImage: fallback.dataset.altImage,
| |
| modelsize: fallback.dataset.modelsize
| |
| });
| |
|
| |
| initPreview(placeholder);
| |
|
| |
| fallback.replaceWith(placeholder);
| |
| }
| |
|
| |
| function scan(root) {
| |
| if (!root) {
| |
| return;
| |
| }
| |
|
| |
| if (
| |
| root.nodeType === Node.ELEMENT_NODE &&
| |
| root.matches('.model-placeholder')
| |
| ) {
| |
| initPreview(root);
| |
| }
| |
|
| |
| if (
| |
| root.nodeType === Node.ELEMENT_NODE &&
| |
| root.matches('.model-fallback')
| |
| ) {
| |
| hydrateFallback(root);
| |
| }
| |
|
| |
| if (root.querySelectorAll) {
| |
| root
| |
| .querySelectorAll('.model-placeholder')
| |
| .forEach(initPreview);
| |
| root
| |
| .querySelectorAll('.model-fallback')
| |
| .forEach(hydrateFallback);
| |
| }
| |
| }
| |
|
| |
| function startObserver() {
| |
| if (!document.body) {
| |
| return;
| |
| }
| |
|
| |
| const observer = new MutationObserver(function (mutations) {
| |
| for (const mutation of mutations) {
| |
| for (const node of mutation.addedNodes) {
| |
| if (node.nodeType === Node.ELEMENT_NODE) {
| |
| scan(node);
| |
| }
| |
| }
| |
| }
| |
| });
| |
|
| |
| observer.observe(document.body, {
| |
| childList: true,
| |
| subtree: true
| |
| });
| |
| }
| |
|
| |
| function init() {
| |
| scan(document);
| |
| startObserver();
| |
| }
| |
|
| |
| mw.hook('wikipage.content').add(function ($content) {
| |
| scan($content[0]);
| |
| });
| |
|
| |
| if (document.readyState === 'loading') {
| |
| document.addEventListener('DOMContentLoaded', init, {
| |
| once: true
| |
| });
| |
| } else {
| |
| init();
| |
| }
| |
|
| |
| })();
| |
|
| |
|
|
| |
|