|
|
| (41 intermediate revisions by 2 users not shown) |
| Line 1: |
Line 1: |
| /* MediaWiki:Common.js is loaded for every page in the wiki. Its functions are FURTHER AUGMENTED BY GADGETS defined in MediaWiki:Gadgets-definition. Where the code is located DOES MATTER, as Gadgets run at the earlier ResourceLoader stage, while code on this file runs later, and should be used for less time-sensitive operations. */ | | /* MediaWiki:Common.js is loaded for every page in the wiki. Its functions are FURTHER AUGMENTED BY GADGETS defined in MediaWiki:Gadgets-definition. In which files the code is located DOES MATTER, as Gadgets run at the earlier ResourceLoader stage, while code on this file runs later, and should be used for less time-sensitive operations. */ |
|
| |
|
| // Load jQuery UI using mw.loader | | // Load jQuery UI using mw.loader |
| Line 5: |
Line 5: |
| console.log("jQuery UI loaded"); | | console.log("jQuery UI loaded"); |
| | | |
| // Initialize and set up responsive handling when DOM is ready | | // Deprecated |
| $(function() { | | $(function() {; |
| setupResponsiveMenus();
| |
| }); | | }); |
|
| |
|
| Line 15: |
Line 14: |
| }); | | }); |
|
| |
|
| // ANCHOR: Include a registration link in the Minerva/mobile hamburger menu for logged out users | | // ANCHOR: Page ID Display |
| mw.loader.using(['mediawiki.util'], function() {
| | $(function() { |
| $(function() { | | // Only show page ID on actual content pages (not special pages, edit pages, etc.) |
| if (mw.config.get('skin') === 'minerva' && mw.user.isAnon()) {
| | var pageId = mw.config.get('wgArticleId'); |
| var waitForPersonalMenu = function(callback) {
| | var namespace = mw.config.get('wgNamespaceNumber'); |
| var $menu = $('#p-personal');
| | var action = mw.config.get('wgAction'); |
| if ($menu.length) {
| | |
| callback($menu);
| | // Show on main namespace (0) and other content namespaces, but not on edit/special pages |
| } else {
| | if (pageId && namespace >= 0 && action === 'view') { |
| setTimeout(function() {
| | // Try to find the footer element (works for both Vector and Minerva skins) |
| waitForPersonalMenu(callback);
| | var $footer = $('#footer.mw-footer, .mw-footer'); |
| }, 100);
| | |
| }
| | if ($footer.length > 0) { |
| };
| | // Insert at the beginning of the footer |
| waitForPersonalMenu(function($menu) {
| | $('<div id="icw-page-id">PageID: ' + pageId + '</div>') |
| var regUrl = mw.util.getUrl('Special:UserLogin', { type: 'signup' });
| | .prependTo($footer); |
| var regText = "Request account";
| | } else { |
| var $regItem = $('<li>')
| | // Fallback: append to body if no footer found |
| .addClass('toggle-list-item menu__item--register')
| | $('<div id="icw-page-id">PageID: ' + pageId + '</div>') |
| .append(
| | .appendTo('body'); |
| $('<a>')
| | } |
| .addClass('toggle-list-item__anchor')
| |
| .attr('href', regUrl)
| |
| .append(
| |
| $('<span class="minerva-icon minerva-icon--logIn"></span>').css('color','var(--general-link-color)')
| |
| )
| |
| .append(
| |
| $('<span class="toggle-list-item__label"></span>').text(regText).css('color','var(--general-link-color)')
| |
| )
| |
| );
| |
| $menu.append($regItem);
| |
| });
| |
| }
| |
| });
| |
| });
| |
| | |
| // ANCHOR: Modified "Gadget-predefined-summaries"
| |
| // Lists interactive common edit reasons below "Summary"
| |
| mw.loader.using(['mediawiki.util']).then(function () {
| |
| 'use strict';
| |
| | |
| if (window.resumeDeluxe === undefined && | |
| ['edit', 'submit'].includes(mw.config.get('wgAction')) &&
| |
| mw.util.getParamValue('section') !== 'new') {
| |
| | |
| var resumeDeluxe = { | |
| titles: ["general fixes", "normalization", "information added", "information update", "categorization", "template fixes", "template update", "+template", "+ references(s)", "+ internal link(s)", "+ external link(s)", "- external link(s)"],
| |
| inputs: ["general fixes", "normalization", "information added", "information update", "categorization", "template fixes", "template update", "+template", "+ references(s)", "+ internal link(s)", "+ external link(s)", "- external link(s)"],
| |
| addToSummary: function (str) {
| |
| var summaryField = document.getElementById('wpSummary');
| |
| if (summaryField) {
| |
| summaryField.value = summaryField.value ? summaryField.value + '; ' + str : str;
| |
| }
| |
| return false;
| |
| }
| |
| };
| |
| | |
| window.resumeDeluxe = resumeDeluxe; | |
| | |
| var DeluxeSummary = function() { | |
| var summaryLabel = document.getElementById('wpSummaryLabel');
| |
| var summaryField = document.getElementById('wpSummary');
| |
| | |
| if (summaryLabel && summaryField) {
| |
| // Prevent duplicate bars by checking for an existing one
| |
| if (summaryLabel.querySelector('.predefined-summaries-bar')) {
| |
| return;
| |
| }
| |
| | |
| // Create container with a unique class name
| |
| var container = document.createElement('span');
| |
| container.className = 'predefined-summaries-bar';
| |
| container.innerHTML = '<b>Predefined summaries</b>: ';
| |
| | |
| resumeDeluxe.titles.forEach((title, index) => { | |
| if (index > 0) {
| |
| container.appendChild(document.createTextNode(' / '));
| |
| }
| |
| | |
| var link = document.createElement('a');
| |
| link.href = '#';
| |
| link.className = 'sumLink';
| |
| link.title = 'Add to edit summary';
| |
| link.textContent = title;
| |
| link.addEventListener('click', function (event) {
| |
| event.preventDefault();
| |
| resumeDeluxe.addToSummary(resumeDeluxe.inputs[index]);
| |
| });
| |
| | |
| container.appendChild(link);
| |
| });
| |
| | |
| container.appendChild(document.createElement('br')); | |
| summaryLabel.prepend(container);
| |
| | |
| // Adjust width as in original script
| |
| summaryField.style.width = '95%';
| |
| }
| |
| }; | |
| | |
| mw.hook('wikipage.content').add(DeluxeSummary);
| |
| } | | } |
| }); | | }); |
|
| |
|
| // ANCHOR: Set "File" Categories intelligently based on file type | | // ANCHOR: Set "File" Categories intelligently based on file type - Incompatible with Gadgets for now |
| mw.loader.using(['mediawiki.util'], function() { | | mw.loader.using(['mediawiki.util'], function () { |
| (function() { | | (function () { |
| // Only run on upload pages | | // Only run on upload pages (classic / MultipleUpload). We do NOT touch UploadWizard here. |
| var page = mw.config.get('wgCanonicalSpecialPageName'); | | var page = mw.config.get('wgCanonicalSpecialPageName'); |
| if ( | | if (window.FileCategoryLoaded || !(/Upload|MultipleUpload/g.test(page))) return; |
| window.FileCategoryLoaded ||
| |
| !(/Upload|MultipleUpload/g.test(page))
| |
| ) {
| |
| return;
| |
| }
| |
| window.FileCategoryLoaded = true; | | window.FileCategoryLoaded = true; |
| | | |
| | // Localized Category namespace label (NS 14) |
| | var CAT_NS = (mw.config.get('wgFormattedNamespaces') || {})[14] || 'Category'; |
| | |
| // Define file type mapping | | // Define file type mapping |
| var categoryMapping = { | | var categoryMapping = { |
| Line 137: |
Line 54: |
| 'jpg': 'Images', | | 'jpg': 'Images', |
| 'jpeg': 'Images', | | 'jpeg': 'Images', |
| 'png': 'Images', | | 'png': 'Images', |
| 'gif': 'Images', | | 'gif': 'Images', |
| 'svg': 'Images', | | 'svg': 'Images', |
| Line 143: |
Line 60: |
| 'bmp': 'Images', | | 'bmp': 'Images', |
| 'tiff': 'Images', | | 'tiff': 'Images', |
|
| | |
| // Documents | | // Documents |
| 'pdf': 'Documents', | | 'pdf': 'Documents', |
| Line 157: |
Line 74: |
| 'ods': 'Documents', | | 'ods': 'Documents', |
| 'odp': 'Documents', | | 'odp': 'Documents', |
|
| | |
| // Archives | | // Archives |
| 'zip': 'Archives', | | 'zip': 'Archives', |
| Line 164: |
Line 81: |
| 'gz': 'Archives', | | 'gz': 'Archives', |
| '7z': 'Archives', | | '7z': 'Archives', |
|
| | |
| // Audio | | // Audio |
| 'mp3': 'Audio', | | 'mp3': 'Audio', |
| Line 170: |
Line 87: |
| 'ogg': 'Audio', | | 'ogg': 'Audio', |
| 'flac': 'Audio', | | 'flac': 'Audio', |
|
| | |
| // Video | | // Video |
| 'mp4': 'Video', | | 'mp4': 'Video', |
| Line 177: |
Line 94: |
| 'mov': 'Video', | | 'mov': 'Video', |
| 'mkv': 'Video', | | 'mkv': 'Video', |
|
| | |
| // Default fallback | | // Default fallback |
| 'default': 'Files' | | 'default': 'Files' |
| }; | | }; |
| | | |
| | function escRx(s) { |
| | return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| | } |
| | |
| // Extract clean extension from filename | | // Extract clean extension from filename |
| function getFileExtension(filename) { | | function getFileExtension(filename) { |
| if (!filename) return ''; | | if (!filename) return ''; |
|
| | var cleanName = filename.split('\\').pop().split('/').pop(); // strip path |
| // Remove path (handle both Windows and Unix paths)
| | var i = cleanName.lastIndexOf('.'); |
| var cleanName = filename.split('\\').pop().split('/').pop(); | | if (i <= 0 || i === cleanName.length - 1) return ''; |
|
| | return cleanName.slice(i + 1).toLowerCase(); |
| // Extract extension
| |
| var parts = cleanName.split('.'); | |
| if (parts.length <= 1) return ''; | |
|
| |
| return parts.pop().toLowerCase(); | |
| } | | } |
|
| | |
| // Apply category based on file extension | | // Apply category based on file extension (writes ONLY to #wpUploadDescription) |
| function applyCategoryFromExtension(filename) { | | function applyCategoryFromExtension(filename) { |
| var $descField = $('#wpUploadDescription'); | | var $descField = $('#wpUploadDescription'); |
| if (!$descField.length || !filename) return; | | if (!$descField.length || !filename) return; |
|
| | |
| var extension = getFileExtension(filename); | | var ext = getFileExtension(filename); |
| if (!extension) return; | | if (!ext) return; |
|
| | |
| var category = categoryMapping[extension] || categoryMapping['default']; | | var category = categoryMapping[ext] || categoryMapping['default']; |
| | | var add = '[[' + CAT_NS + ':' + category + ']]'; |
| // Update the description field, preserving any existing content | | |
| var currentDesc = $descField.val() || ''; | | // Current description |
| | | var current = $descField.val() || ''; |
| // Remove any existing category we might have added before | | |
| Object.values(categoryMapping).forEach(function(catName) { | | // If already present, do nothing (prevents duplicates on repeated change events) |
| var catRegex = new RegExp('\\[\\[Category:' + catName + '\\]\\]', 'g'); | | var already = new RegExp('\\[\\[\\s*' + escRx(CAT_NS) + '\\s*:\\s*' + escRx(category) + '(?:\\s*\\|[^\\]]*)?\\s*\\]\\]', 'i'); |
| currentDesc = currentDesc.replace(catRegex, ''); | | if (already.test(current)) return; |
| | |
| | // Remove any mapped categories we may have added before (handles sort keys and spaces) |
| | var cats = Array.from(new Set(Object.values(categoryMapping))); |
| | cats.forEach(function (catName) { |
| | if (!catName) return; |
| | var rx = new RegExp('\\[\\[\\s*' + escRx(CAT_NS) + '\\s*:\\s*' + escRx(catName) + '(?:\\s*\\|[^\\]]*)?\\s*\\]\\]', 'gi'); |
| | current = current.replace(rx, ''); |
| }); | | }); |
|
| | |
| // Trim any whitespace created by removing categories
| | current = current.trim(); |
| currentDesc = currentDesc.trim(); | | var next = current ? (current + '\n\n' + add) : add; |
|
| | |
| // Add our new category
| | $descField.val(next); |
| var newDesc = currentDesc; | | // console.log('FileCategory: Applied ' + add + ' for .' + ext); |
| if (newDesc) {
| |
| // Add to existing content
| |
| newDesc += '\n\n[[Category:' + category + ']]';
| |
| } else {
| |
| // No content yet
| |
| newDesc = '[[Category:' + category + ']]';
| |
| }
| |
|
| |
| $descField.val(newDesc); | |
| console.log('FileCategory: Applied category ' + category + ' based on extension .' + extension); | |
| } | | } |
|
| | |
| // Set up event listeners | | // Set up event listeners |
| function setupEventListeners() { | | function setupEventListeners() { |
| var $fileInput = $('#wpUploadFile'); | | // Classic uses #wpUploadFile; MultipleUpload often has several inputs (wpUploadFile1, wpUploadFile2, …) |
|
| | var $fileInputs = $('#wpUploadFile, [id^="wpUploadFile"]'); |
| // Skip if we can't find the file input
| | |
| if (!$fileInput.length) return; | | if (!$fileInputs.length) return; |
|
| | |
| // Listen for file selection changes | | // Listen for file selection changes on all inputs |
| $fileInput.on('change', function() { | | $fileInputs.on('change', function () { |
| var filename = $(this).val(); | | var filename = $(this).val(); |
| if (filename) { | | if (filename) applyCategoryFromExtension(filename); |
| applyCategoryFromExtension(filename);
| |
| }
| |
| }); | | }); |
|
| | |
| // Also hook into MediaWiki's upload events if available | | // Hook into MediaWiki's upload events if available |
| if (typeof mw.hook !== 'undefined') { | | if (typeof mw.hook !== 'undefined') { |
| mw.hook('uploadform.fileSelected').add(function(data) { | | mw.hook('uploadform.fileSelected').add(function (data) { |
| if (data && data.filename) { | | if (data && data.filename) applyCategoryFromExtension(data.filename); |
| applyCategoryFromExtension(data.filename);
| |
| }
| |
| }); | | }); |
| } | | } |
|
| | |
| // For AJAX-based uploads that might update the file input after page load | | // Observe value attribute changes (some browsers/flows update programmatically) |
| var observer = new MutationObserver(function(mutations) { | | $fileInputs.each(function () { |
| mutations.forEach(function(mutation) {
| | var el = this; |
| if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
| | if (!window.MutationObserver) return; |
| var filename = $fileInput.val();
| | try { |
| if (filename) {
| | var ob = new MutationObserver(function (mutations) { |
| applyCategoryFromExtension(filename);
| | for (var i = 0; i < mutations.length; i++) { |
| | var m = mutations[i]; |
| | if (m.type === 'attributes' && m.attributeName === 'value') { |
| | var filename = $(el).val(); |
| | if (filename) applyCategoryFromExtension(filename); |
| | } |
| } | | } |
| } | | }); |
| }); | | ob.observe(el, { attributes: true, attributeFilter: ['value'] }); |
| | } catch (e) { /* no-op */ } |
| }); | | }); |
|
| |
| // Observe the file input for value changes
| |
| observer.observe($fileInput[0], { attributes: true });
| |
| } | | } |
|
| | |
| // Initialize when DOM is ready | | // Initialize when DOM is ready |
| $(setupEventListeners); | | $(setupEventListeners); |
| })(); | | })(); |
| }); | | }); |
|
| |
| // ANCHOR: Responsive expanding menus
| |
| /* REVIEW: Codex */
| |
| var menuState = {
| |
| initialized: false,
| |
| isCurrentlyMobile: false,
| |
| originalTopRow: null,
| |
| originalPrimaryMenu: null,
| |
| originalSecondaryMenu: null,
| |
| accordionState: {},
| |
| hasLoggedSetupUnnecessary: false // Track if we've already logged setup unnecessary message
| |
| };
| |
|
| |
| // Media query for mobile breakpoint
| |
| var mobileMediaQuery = window.matchMedia("(max-width: 41rem)");
| |
|
| |
| // Main setup function for responsive menu behavior: once per page load
| |
| function setupResponsiveMenus() {
| |
| var $topRow = $('.icannwiki-top-row');
| |
|
| |
| // Only proceed if we have the top row
| |
| if ($topRow.length === 0) {
| |
| // Only log if we haven't already logged this message
| |
| if (!menuState.hasLoggedSetupUnnecessary) {
| |
| console.log("Menu setup unnecessary");
| |
| menuState.hasLoggedSetupUnnecessary = true;
| |
| }
| |
| return;
| |
| }
| |
|
| |
| // Capture the original DOM state and save it
| |
| if (!menuState.initialized) {
| |
| // Store original DOM state references (clone deeply to preserve all attributes and content)
| |
| menuState.originalTopRow = $topRow.clone(true, true);
| |
| menuState.originalPrimaryMenu = $topRow.find('.icannwiki-menu-container.primary').clone(true, true);
| |
| menuState.originalSecondaryMenu = $topRow.find('.icannwiki-menu-container.secondary').clone(true, true);
| |
| menuState.initialized = true;
| |
| console.log("Original menu state captured");
| |
| }
| |
|
| |
| // Setup viewport change listener for future screen size changes
| |
| try {
| |
| mobileMediaQuery.removeEventListener('change', handleViewportChange);
| |
| } catch (e) {
| |
| // Ignore any errors from trying to remove a non-existent listener
| |
| }
| |
|
| |
| // Setup listener for future viewport changes
| |
| mobileMediaQuery.addEventListener('change', handleViewportChange);
| |
|
| |
| // Initial layout setup based on current viewport at page load
| |
| setupInitialLayout(mobileMediaQuery.matches);
| |
| }
| |
|
| |
| // Initial layout setup function: once per page load
| |
| function setupInitialLayout(isMobile) {
| |
| menuState.isCurrentlyMobile = isMobile;
| |
|
| |
| if (isMobile) {
| |
| // First create the mobile accordions directly and completely
| |
| initMobileLayoutImmediately();
| |
| }
| |
| // Desktop layout is the default, no need to do anything
| |
| }
| |
|
| |
| // Handle viewport size changes after initial load
| |
| function handleViewportChange(mediaQuery) {
| |
| if (mediaQuery.matches && !menuState.isCurrentlyMobile) {
| |
| // Switching desktop -> mobile
| |
| menuState.isCurrentlyMobile = true;
| |
| initMobileLayoutImmediately();
| |
| } else if (!mediaQuery.matches && menuState.isCurrentlyMobile) {
| |
| // Switching mobile -> desktop
| |
| menuState.isCurrentlyMobile = false;
| |
| restoreDesktopLayout();
| |
| }
| |
| // If already in the correct layout mode, do nothing to avoid flashing
| |
| }
| |
|
| |
| // Create mobile layout immediately without any timeouts
| |
| function initMobileLayoutImmediately() {
| |
| console.log("Setting up mobile accordions");
| |
|
| |
| // Get reference to the top row
| |
| var $topRow = $('.icannwiki-top-row');
| |
|
| |
| // If the top row is already set up as accordions, don't recreate them
| |
| if ($topRow.find('.icannwiki-accordion-section').length > 0) {
| |
| console.log("Accordions already exist, no need to reinitialize");
| |
|
| |
| // Make sure the top row is visible
| |
| if (!$topRow.hasClass('mobile-ready')) {
| |
| $topRow.addClass('mobile-ready');
| |
| }
| |
| return;
| |
| }
| |
|
| |
| // Ensure we have an initialized state
| |
| if (!menuState.initialized) {
| |
| console.error("Menu state not initialized, cannot create accordions");
| |
| return;
| |
| }
| |
|
| |
| // Store any existing accordion open/closed states
| |
| $('.icannwiki-accordion-header').each(function() {
| |
| var $header = $(this);
| |
| var isActive = $header.hasClass('active');
| |
| var text = $header.text().trim();
| |
| menuState.accordionState[text] = isActive;
| |
| });
| |
|
| |
| // Get fresh copies of the original menu elements
| |
| var $primaryMenu = menuState.originalPrimaryMenu.clone(true, true);
| |
| var $secondaryMenu = menuState.originalSecondaryMenu.clone(true, true);
| |
|
| |
| // Clear the top row in preparation for adding accordions
| |
| $topRow.empty();
| |
|
| |
| // Build complete accordion structure with content in HTML
| |
| var accordionHTML =
| |
| '<div class="icannwiki-accordion-section">' +
| |
| '<button type="button" class="icannwiki-accordion-header primary" aria-expanded="false">ICANN</button>' +
| |
| '<div class="icannwiki-accordion-content" aria-hidden="true"></div>' +
| |
| '</div>' +
| |
| '<div class="icannwiki-accordion-section">' +
| |
| '<button type="button" class="icannwiki-accordion-header secondary" aria-expanded="false">Internet Governance</button>' +
| |
| '<div class="icannwiki-accordion-content" aria-hidden="true"></div>' +
| |
| '</div>';
| |
|
| |
| // Append the full accordion structure at once
| |
| $topRow.append(accordionHTML);
| |
|
| |
| // Insert the original menu content into the accordion containers
| |
| $topRow.find('.icannwiki-accordion-section:eq(0) .icannwiki-accordion-content').append($primaryMenu);
| |
| $topRow.find('.icannwiki-accordion-section:eq(1) .icannwiki-accordion-content').append($secondaryMenu);
| |
|
| |
| // Add click handlers for accordion toggles
| |
| $('.icannwiki-accordion-header').on('click', function() {
| |
| var $header = $(this);
| |
| var $content = $header.next('.icannwiki-accordion-content');
| |
| var isOpen = $header.hasClass('active');
| |
|
| |
| // Store the state for persistence across viewport changes
| |
| menuState.accordionState[$header.text().trim()] = !isOpen;
| |
|
| |
| // Toggle active state
| |
| $header.toggleClass('active');
| |
| $content.toggleClass('active');
| |
|
| |
| // Update ARIA attributes
| |
| $header.attr('aria-expanded', !isOpen);
| |
| $content.attr('aria-hidden', isOpen);
| |
| });
| |
|
| |
| // Restore any previously open accordions
| |
| $('.icannwiki-accordion-header').each(function() {
| |
| var $header = $(this);
| |
| var text = $header.text().trim();
| |
|
| |
| if (menuState.accordionState[text]) {
| |
| $header.addClass('active');
| |
| $header.next('.icannwiki-accordion-content').addClass('active');
| |
| $header.attr('aria-expanded', 'true');
| |
| $header.next('.icannwiki-accordion-content').attr('aria-hidden', 'false');
| |
| }
| |
| });
| |
|
| |
| // Make the top row visible now that it's fully built
| |
| $topRow.addClass('mobile-ready');
| |
| }
| |
|
| |
| // Function to restore desktop layout
| |
| function restoreDesktopLayout() {
| |
| console.log("Restoring desktop layout");
| |
|
| |
| var $topRow = $('.icannwiki-top-row');
| |
|
| |
| // If no accordions present, assume already in desktop layout
| |
| if ($topRow.find('.icannwiki-accordion-section').length === 0) {
| |
| console.log("Already in desktop layout");
| |
| return;
| |
| }
| |
|
| |
| // Get fresh copies of the original menus
| |
| var $primaryMenu = menuState.originalPrimaryMenu.clone(true, true);
| |
| var $secondaryMenu = menuState.originalSecondaryMenu.clone(true, true);
| |
|
| |
| // Clear top row and append original menus in one operation
| |
| $topRow.empty()
| |
| .append($primaryMenu)
| |
| .append($secondaryMenu)
| |
| .removeClass('mobile-ready');
| |
| }
| |
| // Initialize on page load
| |
| setupResponsiveMenus();
| |
|
| |
| // ANCHOR: ElementPortraitCarousel dynamics
| |
| $(function() {
| |
| // Only execute if there are any ".person-portrait-carousel" on the page
| |
| if ($('.person-portrait-carousel').length === 0) return;
| |
|
| |
| console.log('Initializing person template carousel');
| |
|
| |
| // Initialize all carousels on the page
| |
| $('.carousel-container').each(function() {
| |
| var $container = $(this);
| |
| var $items = $container.find('.carousel-item');
| |
|
| |
| // Skip if no items
| |
| if ($items.length === 0) return;
| |
|
| |
| // Wait for images to load
| |
| $items.find('img').on('load', function() {
| |
| $(this).data('loaded', true);
| |
| }).each(function() {
| |
| // If already complete, trigger load event
| |
| if (this.complete) {
| |
| $(this).trigger('load');
| |
| }
| |
| });
| |
|
| |
| // Initial positioning of carousel items
| |
| function initializeCarousel() {
| |
| // Show first image, position others
| |
| $items.eq(0).addClass('carousel-visible').removeClass('carousel-hidden');
| |
|
| |
| // Logic for exactly 2 images (orbital layout)
| |
| if ($items.length === 2) {
| |
| $items.eq(0).addClass('carousel-visible carousel-orbital-1').removeClass('carousel-hidden carousel-orbital-2 carousel-left carousel-right');
| |
| $items.eq(1).addClass('carousel-hidden carousel-orbital-2').removeClass('carousel-visible carousel-orbital-1 carousel-left carousel-right');
| |
| }
| |
| // Logic for 3+ images (card shuffle)
| |
| else if ($items.length > 2) {
| |
| $items.eq(1).addClass('carousel-right').removeClass('carousel-left carousel-visible');
| |
| $items.eq($items.length - 1).addClass('carousel-left').removeClass('carousel-right carousel-visible');
| |
| }
| |
|
| |
| // Let images dimension within the fixed container
| |
| $container.find('.carousel-item img').css({
| |
| 'max-height': '100%',
| |
| 'max-width': '100%',
| |
| 'object-fit': 'contain'
| |
| });
| |
| }
| |
|
| |
| // Initialize carousel after a delay to ensure proper rendering
| |
| setTimeout(initializeCarousel, 100);
| |
| });
| |
|
| |
| // Click handler for buttons
| |
| $('.carousel-next').on('click', function() {
| |
| var $container = $(this).closest('.carousel-container');
| |
| var $items = $container.find('.carousel-item');
| |
| var $current = $container.find('.carousel-visible');
| |
| var currentIndex = parseInt($current.data('index'));
| |
| var nextIndex = (currentIndex % $items.length) + 1;
| |
| var $next = $container.find('.carousel-item[data-index="' + nextIndex + '"]');
| |
|
| |
| // Check if there are exactly 2 images
| |
| if ($items.length === 2) {
| |
| // Clear any existing classes and animations
| |
| $items.removeClass('carousel-left carousel-right orbital-animating-forward orbital-animating-backward');
| |
|
| |
| // Apply the animation class to create orbital motion
| |
| $current.addClass('orbital-animating-forward');
| |
| $next.addClass('orbital-animating-backward');
| |
|
| |
| // After animation completes, update the final state classes
| |
| setTimeout(function() {
| |
| $current.removeClass('carousel-visible carousel-orbital-1 orbital-animating-forward')
| |
| .addClass('carousel-hidden carousel-orbital-2');
| |
| $next.removeClass('carousel-hidden carousel-orbital-2 orbital-animating-backward')
| |
| .addClass('carousel-visible carousel-orbital-1');
| |
| }, 600); // Match CSS animation duration (0.6s)
| |
| } else {
| |
| // Logic for 3+ images
| |
| // Remove position classes
| |
| $items.removeClass('carousel-left carousel-right');
| |
|
| |
| // Removals and additions
| |
| $current.removeClass('carousel-visible').addClass('carousel-hidden carousel-left');
| |
| $next.removeClass('carousel-hidden carousel-left carousel-right').addClass('carousel-visible');
| |
|
| |
| // Set the item after next to right
| |
| var afterNextIndex = (nextIndex % $items.length) + 1;
| |
| if (afterNextIndex > $items.length) afterNextIndex = 1;
| |
| var $afterNext = $container.find('.carousel-item[data-index="' + afterNextIndex + '"]');
| |
| if ($afterNext.length && !$afterNext.is($next)) {
| |
| $afterNext.removeClass('carousel-visible carousel-left').addClass('carousel-hidden carousel-right');
| |
| }
| |
| }
| |
| });
| |
|
| |
| // Click handler for the previous button for 2-image case
| |
| $('.carousel-prev').on('click', function() {
| |
| // Get container and items
| |
| var $container = $(this).closest('.carousel-container');
| |
| var $items = $container.find('.carousel-item');
| |
| var $current = $container.find('.carousel-visible');
| |
|
| |
| // Calculate previous index
| |
| var currentIndex = parseInt($current.data('index'));
| |
| var prevIndex = currentIndex - 1;
| |
| if (prevIndex < 1) prevIndex = $items.length;
| |
| var $prev = $container.find('.carousel-item[data-index="' + prevIndex + '"]');
| |
|
| |
| // Check if we have exactly 2 images: use reversed logic compared to next button
| |
| if ($items.length === 2) {
| |
| // Clear any existing classes and animations
| |
| $items.removeClass('carousel-left carousel-right orbital-animating-forward orbital-animating-backward');
| |
|
| |
| // Apply correct animation classes for leftward movement
| |
| $current.addClass('orbital-animating-forward'); // Front item moves back
| |
| $prev.addClass('orbital-animating-backward'); // Back item moves forward
| |
|
| |
| // Update final state and remove correct animation classes
| |
| var updateFinalState = function() {
| |
| $current.removeClass('carousel-visible carousel-orbital-1 orbital-animating-forward') // Remove forward animation class
| |
| .addClass('carousel-hidden carousel-orbital-2');
| |
| $prev.removeClass('carousel-hidden carousel-orbital-2 orbital-animating-backward') // Remove backward animation class
| |
| .addClass('carousel-visible carousel-orbital-1');
| |
| };
| |
|
| |
| // Match CSS animation duration (0.6s)
| |
| setTimeout(updateFinalState, 600);
| |
| } else {
| |
| // Logic for 3+ images (card shuffle)
| |
| // Remove position classes
| |
| $items.removeClass('carousel-left carousel-right');
| |
|
| |
| // Removals and additions
| |
| $current.removeClass('carousel-visible').addClass('carousel-hidden carousel-right');
| |
| $prev.removeClass('carousel-hidden carousel-left carousel-right').addClass('carousel-visible');
| |
|
| |
| // Set the item before prev to left
| |
| var beforePrevIndex = prevIndex - 1;
| |
| if (beforePrevIndex < 1) beforePrevIndex = $items.length;
| |
| var $beforePrev = $container.find('.carousel-item[data-index="' + beforePrevIndex + '"]');
| |
| if ($beforePrev.length && !$beforePrev.is($prev)) {
| |
| $beforePrev.removeClass('carousel-visible carousel-right').addClass('carousel-hidden carousel-left');
| |
| }
| |
| }
| |
| });
| |
|
| |
| // Touch swipe support for mobile users
| |
| $('.carousel-container').each(function() {
| |
| var $container = $(this);
| |
| var startX, startY, endX, endY;
| |
| var MIN_SWIPE_DISTANCE = 50; // Minimum distance for a swipe to be registered
| |
| var MAX_VERTICAL_DISTANCE = 50; // Maximum vertical movement allowed for horizontal swipe
| |
|
| |
| // Touch start: record initial position
| |
| $container.on('touchstart', function(e) {
| |
| // Store initial touch coordinates
| |
| startX = e.originalEvent.touches[0].clientX;
| |
| startY = e.originalEvent.touches[0].clientY;
| |
| });
| |
|
| |
| // Touch end: determine swipe direction and trigger appropriate action
| |
| $container.on('touchend', function(e) {
| |
| // Get final touch coordinates
| |
| endX = e.originalEvent.changedTouches[0].clientX;
| |
| endY = e.originalEvent.changedTouches[0].clientY;
| |
|
| |
| // Calculate horizontal and vertical distance
| |
| var horizontalDistance = endX - startX;
| |
| var verticalDistance = Math.abs(endY - startY);
| |
|
| |
| // Only register as swipe if horizontal movement is significant and vertical movement is limited
| |
| if (Math.abs(horizontalDistance) >= MIN_SWIPE_DISTANCE && verticalDistance <= MAX_VERTICAL_DISTANCE) {
| |
| // Prevent default behavior if it's a horizontal swipe
| |
| e.preventDefault();
| |
|
| |
| if (horizontalDistance > 0) {
| |
| // Swipe right: go to previous slide
| |
| $container.find('.carousel-prev').trigger('click');
| |
| } else {
| |
| // Swipe left: go to next slide
| |
| $container.find('.carousel-next').trigger('click');
| |
| }
| |
| }
| |
| });
| |
| });
| |
|
| |
| console.log('Template:Person carousel initialization successful');
| |
| });
| |
| });
| |
|
| |
| // ANCHOR: Back‑to‑Top button with sentinel trigger
| |
| (function ($, mw) {
| |
| 'use strict';
| |
| if (window.ICWBackToTopLoaded) return;
| |
| window.ICWBackToTopLoaded = true;
| |
|
| |
| const scrollMs = 600; // smooth‑scroll duration
| |
|
| |
| /* ---------------------------------------------------------
| |
| 1. Create button (SVG arrow already optically centred)
| |
| --------------------------------------------------------- */
| |
| const svg = `
| |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
| |
| viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"
| |
| stroke-linecap="round" stroke-linejoin="round">
| |
| <g transform="translate(1,0)"> <!-- 1 px optical shift -->
| |
| <polyline points="6 9 12 3 18 9"/>
| |
| <line x1="12" y1="21" x2="12" y2="3"/>
| |
| </g>
| |
| </svg>`;
| |
|
| |
| const $btn = $('<a>', {
| |
| id: 'icw-back-to-top',
| |
| href: '#',
| |
| 'aria-label': 'Back to top',
| |
| html: svg
| |
| }).appendTo(document.body);
| |
|
| |
| /* ---------------------------------------------------------
| |
| 2. Insert a sentinel right before the article title
| |
| (if not found, fall back to body top)
| |
| --------------------------------------------------------- */
| |
| const sentinel = document.createElement('div');
| |
| sentinel.style.position = 'absolute';
| |
| sentinel.style.top = 0;
| |
| sentinel.style.width = '1px';
| |
| sentinel.style.height = '1px';
| |
|
| |
| const firstHeading = document.getElementById('firstHeading');
| |
| if (firstHeading && firstHeading.parentNode) {
| |
| firstHeading.parentNode.insertBefore(sentinel, firstHeading);
| |
| } else {
| |
| document.body.prepend(sentinel);
| |
| }
| |
|
| |
| /* ---------------------------------------------------------
| |
| 3. Show / hide button based on sentinel visibility
| |
| --------------------------------------------------------- */
| |
| new IntersectionObserver(
| |
| ([entry]) => {
| |
| $btn.toggleClass('is-visible', !entry.isIntersecting);
| |
| },
| |
| {root: null, threshold: 0}
| |
| ).observe(sentinel);
| |
|
| |
| /* ---------------------------------------------------------
| |
| 4. Smooth scroll on click
| |
| --------------------------------------------------------- */
| |
| $btn.on('click.icwBTT', e => {
| |
| e.preventDefault();
| |
| $('html, body').animate({scrollTop: 0}, scrollMs);
| |
| });
| |
|
| |
| })(jQuery, mediaWiki);
| |
|
| |
|
| // ANCHOR: Template debug monitor | | // ANCHOR: Template debug monitor |
| Line 965: |
Line 425: |
| }); | | }); |
|
| |
|
| // ANCHOR: Page ID Display
| |
| $(function() {
| |
| // Only show page ID on actual content pages (not special pages, edit pages, etc.)
| |
| var pageId = mw.config.get('wgArticleId');
| |
| var namespace = mw.config.get('wgNamespaceNumber');
| |
| var action = mw.config.get('wgAction');
| |
|
| |
| // Show on main namespace (0) and other content namespaces, but not on edit/special pages
| |
| if (pageId && namespace >= 0 && action === 'view') {
| |
| // Try to find the footer element (works for both Vector and Minerva skins)
| |
| var $footer = $('#footer.mw-footer, .mw-footer');
| |
|
| |
| if ($footer.length > 0) {
| |
| // Insert at the beginning of the footer
| |
| $('<div id="icw-page-id">PageID: ' + pageId + '</div>')
| |
| .prependTo($footer);
| |
| } else {
| |
| // Fallback: append to body if no footer found
| |
| $('<div id="icw-page-id">PageID: ' + pageId + '</div>')
| |
| .appendTo('body');
| |
| }
| |
| }
| |
| });
| |
|
| |
| // ANCHOR: Page Creator
| |
| // SHARED CODEBASE
| |
| function fetchTemplateList(callback, errorCallback) {
| |
| var api = new mw.Api();
| |
| api.parse('{{#invoke:TemplateStarter|listTemplates}}')
| |
| .done(function(html) {
| |
| var tempDiv = document.createElement('div');
| |
| tempDiv.innerHTML = html;
| |
| var templateListText = tempDiv.textContent || tempDiv.innerText || '';
| |
| var availableTemplates = templateListText.split(',').map(function(t) {
| |
| return t.trim().split('\n')[0].trim();
| |
| }).filter(function(t) {
| |
| return t.length > 0 && t.match(/^[a-zA-Z][a-zA-Z0-9_\-\s\(\)]*$/);
| |
| });
| |
| callback(availableTemplates);
| |
| })
| |
| .fail(function(error) {
| |
| if (errorCallback) errorCallback(error);
| |
| });
| |
| }
| |
|
| |
| function fetchTemplateContent(templateType, callback, errorCallback) {
| |
| var api = new mw.Api();
| |
| api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
| |
| .done(function(html) {
| |
| var tempDiv = document.createElement('div');
| |
| tempDiv.innerHTML = html;
| |
| var templateContent = tempDiv.textContent || tempDiv.innerText || '';
| |
| callback(templateContent);
| |
| })
| |
| .fail(function(error) {
| |
| if (errorCallback) errorCallback(error);
| |
| });
| |
| }
| |
|
| |
| function cleanupUnfilledPlaceholders(templateContent, pageName, formData) {
| |
| var processedContent = templateContent;
| |
| if (!pageName || pageName.trim() === '') {
| |
| throw new Error('Page name is required and cannot be empty');
| |
| }
| |
| processedContent = processedContent.replace(/\$PAGE_NAME\$/g, pageName);
| |
| for (var key in formData) {
| |
| if (key !== 'PAGE_NAME' && formData[key] && formData[key].trim() !== '') {
| |
| var placeholder = '$' + key + '$';
| |
| var escapedPlaceholder = placeholder.replace(/\$/g, '\\$');
| |
| processedContent = processedContent.replace(new RegExp(escapedPlaceholder, 'g'), formData[key]);
| |
| }
| |
| }
| |
| processedContent = processedContent.replace(/\$[A-Z_]+\$/g, '');
| |
| processedContent = processedContent.replace(/\s+/g, ' ');
| |
| processedContent = processedContent.replace(/is a\s+based/g, 'is based');
| |
| processedContent = processedContent.replace(/is a\s+in/g, 'is in');
| |
| processedContent = processedContent.replace(/\s+\./g, '.');
| |
| processedContent = processedContent.trim();
| |
| return processedContent;
| |
| }
| |
|
| |
| // HERO PAGE CREATOR WIDGET
| |
| mw.loader.using(['mediawiki.api', 'mediawiki.util'], function() {
| |
| $(function() {
| |
| // Only run if the hero page creator container exists
| |
| if ($('#hero-page-creator').length === 0) return;
| |
|
| |
| console.log('Initializing page creator widget');
| |
|
| |
| // Build the simple HTML form
| |
| var formHtml =
| |
| '<div class="icannwiki-search-unifier">' +
| |
| '<div class="mw-inputbox-element">' +
| |
| '<input type="text" id="hero-page-name" class="searchboxInput" placeholder="Page name">' +
| |
| '<select id="hero-template-type" class="searchboxInput" disabled>' +
| |
| '<option value="">Loading...</option>' +
| |
| '</select>' +
| |
| '<button id="hero-create-btn" class="mw-ui-button mw-ui-progressive" disabled>Create</button>' +
| |
| '</div>' +
| |
| '</div>';
| |
|
| |
| $('#hero-page-creator').html(formHtml);
| |
|
| |
| // Fetch available templates using shared function
| |
| fetchTemplateList(
| |
| function(availableTemplates) {
| |
| var $select = $('#hero-template-type');
| |
| $select.empty().append('<option value="">Select a subject (template)</option>');
| |
| availableTemplates.forEach(function(template) {
| |
| $select.append('<option value="' + template + '">' + template + '</option>');
| |
| });
| |
| $select.prop('disabled', false);
| |
| },
| |
| function() {
| |
| $('#hero-template-type').empty().append('<option value="">Error</option>');
| |
| }
| |
| );
| |
|
| |
| // Enable/disable create button
| |
| function updateHeroCreateButton() {
| |
| var pageName = $('#hero-page-name').val().trim();
| |
| var templateType = $('#hero-template-type').val();
| |
| $('#hero-create-btn').prop('disabled', !pageName || !templateType);
| |
| }
| |
|
| |
| $('#hero-page-name, #hero-template-type').on('input change', updateHeroCreateButton);
| |
|
| |
| // Handle create button click
| |
| $('#hero-create-btn').on('click', function() {
| |
| var pageName = $('#hero-page-name').val().trim();
| |
| var templateType = $('#hero-template-type').val();
| |
|
| |
| if (!pageName || !templateType) return;
| |
|
| |
| $(this).prop('disabled', true).text('Creating...');
| |
|
| |
| fetchTemplateContent(templateType, function(templateContent) {
| |
| try {
| |
| var formData = {};
| |
| templateContent = cleanupUnfilledPlaceholders(templateContent, pageName, formData);
| |
| } catch (error) {
| |
| alert('Error: ' + error.message);
| |
| $('#hero-create-btn').prop('disabled', false).text('Create');
| |
| return;
| |
| }
| |
|
| |
| var api = new mw.Api();
| |
| api.create(pageName, { summary: 'Creating new ' + templateType + ' page' }, templateContent)
| |
| .done(function() {
| |
| window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
| |
| })
| |
| .fail(function(code) {
| |
| if (code === 'articleexists') {
| |
| if (confirm('Page already exists. Do you want to edit it?')) {
| |
| window.location.href = mw.util.getUrl(pageName, { action: 'edit' });
| |
| }
| |
| } else {
| |
| alert('Error creating page: ' + code);
| |
| }
| |
| $('#hero-create-btn').prop('disabled', false).text('Create');
| |
| });
| |
| }, function() {
| |
| alert('Error loading template content.');
| |
| $('#hero-create-btn').prop('disabled', false).text('Create');
| |
| });
| |
| });
| |
| });
| |
| });
| |
|
| |
| // CREATE A PAGE METAPAGE
| |
| mw.loader.using(['mediawiki.api', 'mediawiki.util'], function() {
| |
| $(function() {
| |
| // Only run on pages that have the template creator container
| |
| if ($('#template-creator-container').length === 0) return;
| |
|
| |
| console.log('Initializing TemplateStarter');
| |
|
| |
| // Build the initial HTML with loading state
| |
| var formHtml =
| |
| '<div class="template-creator-main-layout">' +
| |
| '<div class="template-creator-form-container">' +
| |
| '<h3>Create New Page</h3>' +
| |
| '<div class="form-group">' +
| |
| '<label for="template-type">Template Type:</label>' +
| |
| '<select id="template-type" class="form-control" disabled>' +
| |
| '<option value="">Loading templates...</option>' +
| |
| '</select>' +
| |
| '</div>' +
| |
| '<div class="form-group">' +
| |
| '<label for="page-name">Page Name:</label>' +
| |
| '<input type="text" id="page-name" class="form-control" placeholder="Enter page name...">' +
| |
| '</div>' +
| |
| '<div id="dynamic-fields-container"></div>' +
| |
| '<button id="create-page-btn" class="mw-ui-button mw-ui-progressive" disabled>Create Page</button>' +
| |
| '</div>' +
| |
| '<div id="template-preview" class="template-creator-preview-container" style="display:none;">' +
| |
| '<h4>Preview:</h4>' +
| |
| '<pre id="template-preview-content"></pre>' +
| |
| '</div>' +
| |
| '</div>';
| |
|
| |
| // Insert the form
| |
| $('#template-creator-container').html(formHtml);
| |
|
| |
| // Fetch available templates using shared function
| |
| fetchTemplateList(
| |
| function(availableTemplates) {
| |
| var $select = $('#template-type');
| |
| $select.empty().append('<option value="">Select a template...</option>');
| |
| availableTemplates.forEach(function(template) {
| |
| $select.append('<option value="' + template + '">' + template + '</option>');
| |
| });
| |
| $select.prop('disabled', false);
| |
| console.log('Loaded templates dynamically:', availableTemplates);
| |
| },
| |
| function(error) {
| |
| console.error('Failed to load templates dynamically:', error);
| |
| $('#template-type').empty().append('<option value="">Error loading templates</option>').prop('disabled', true);
| |
| }
| |
| );
| |
|
| |
| // Enable/disable create button based on form completion
| |
| function updateCreateButton() {
| |
| var templateType = $('#template-type').val();
| |
| var pageName = $('#page-name').val().trim();
| |
| $('#create-page-btn').prop('disabled', !templateType || !pageName);
| |
| }
| |
|
| |
| // Update button state when inputs change
| |
| $('#template-type, #page-name').on('change input', updateCreateButton);
| |
|
| |
| // Preview template when type is selected
| |
| $('#template-type').on('change', function() {
| |
| var templateType = $(this).val();
| |
| if (!templateType) {
| |
| $('#template-preview').hide();
| |
| $('#dynamic-fields-container').empty();
| |
| return;
| |
| }
| |
|
| |
| // Load dynamic fields for this template type
| |
| loadDynamicFields(templateType);
| |
| updatePreview();
| |
| });
| |
|
| |
| // Update preview when page name changes
| |
| $('#page-name').on('input', function() {
| |
| var templateType = $('#template-type').val();
| |
| if (templateType) {
| |
| updatePreview();
| |
| }
| |
| });
| |
|
| |
| // Function to update the preview with processed placeholders
| |
| function updatePreview() {
| |
| var templateType = $('#template-type').val();
| |
| var pageName = $('#page-name').val().trim();
| |
|
| |
| if (!templateType) return;
| |
|
| |
| // Call the Lua module to get template structure
| |
| var api = new mw.Api();
| |
| api.parse('{{#invoke:TemplateStarter|preload|' + templateType + '}}')
| |
| .done(function(html) {
| |
| // Extract text content from the parsed HTML
| |
| var tempDiv = document.createElement('div');
| |
| tempDiv.innerHTML = html;
| |
| var templateContent = tempDiv.textContent || tempDiv.innerText || '';
| |
|
| |
| // Collect form data and process placeholders
| |
| var formData = collectFormData();
| |
| templateContent = processPlaceholders(templateContent, pageName, formData);
| |
|
| |
| $('#template-preview-content').text(templateContent);
| |
| $('#template-preview').show();
| |
| })
| |
| .fail(function(error) {
| |
| console.error('Failed to preview template:', error);
| |
| $('#template-preview-content').text('Error loading template preview');
| |
| $('#template-preview').show();
| |
| });
| |
| }
| |
|
| |
| // Function to load dynamic fields for a template type
| |
| function loadDynamicFields(templateType) {
| |
| console.log('Loading dynamic fields for template:', templateType);
| |
|
| |
| var api = new mw.Api();
| |
| api.parse('{{#invoke:TemplateStarter|getCreatorFieldDefinitionsJSON|' + templateType + '}}')
| |
| .done(function(html) {
| |
| // Extract JSON content from the parsed HTML
| |
| var tempDiv = document.createElement('div');
| |
| tempDiv.innerHTML = html;
| |
| var jsonText = tempDiv.textContent || tempDiv.innerText || '';
| |
|
| |
| try {
| |
| var fieldDefinitions = JSON.parse(jsonText);
| |
| console.log('Field definitions loaded:', fieldDefinitions);
| |
|
| |
| // Generate form fields
| |
| generateDynamicFields(fieldDefinitions);
| |
| } catch (e) {
| |
| console.error('Failed to parse field definitions JSON:', e, jsonText);
| |
| $('#dynamic-fields-container').html('<p style="color: red;">Error loading field definitions</p>');
| |
| }
| |
| })
| |
| .fail(function(error) {
| |
| console.error('Failed to load field definitions:', error);
| |
| $('#dynamic-fields-container').html('<p style="color: red;">Error loading field definitions</p>');
| |
| });
| |
| }
| |
|
| |
| // Function to generate dynamic form fields
| |
| function generateDynamicFields(fieldDefinitions) {
| |
| var $container = $('#dynamic-fields-container');
| |
| $container.empty();
| |
|
| |
| // Skip PAGE_NAME as it's handled separately
| |
| for (var fieldKey in fieldDefinitions) {
| |
| if (fieldKey === 'PAGE_NAME') continue;
| |
|
| |
| var fieldDef = fieldDefinitions[fieldKey];
| |
| var $formGroup = $('<div class="form-group"></div>');
| |
|
| |
| // Create label
| |
| var $label = $('<label></label>')
| |
| .attr('for', 'dynamic-field-' + fieldKey)
| |
| .text(fieldDef.label);
| |
|
| |
| // Create input
| |
| var $input = $('<input type="text" class="form-control dynamic-field">')
| |
| .attr('id', 'dynamic-field-' + fieldKey)
| |
| .attr('data-field-key', fieldKey)
| |
| .attr('placeholder', fieldDef.placeholder);
| |
|
| |
| // Mark required fields
| |
| if (fieldDef.required) {
| |
| $label.append(' <span style="color: red;">*</span>');
| |
| $input.attr('required', true);
| |
| }
| |
|
| |
| $formGroup.append($label).append($input);
| |
| $container.append($formGroup);
| |
|
| |
| // Add event listener for preview updates
| |
| $input.on('input', function() {
| |
| var templateType = $('#template-type').val();
| |
| if (templateType) {
| |
| updatePreview();
| |
| }
| |
| });
| |
| }
| |
|
| |
| // Update create button state when dynamic fields change
| |
| $container.on('input', '.dynamic-field', updateCreateButton);
| |
|
| |
| console.log('Dynamic fields generated');
| |
| }
| |
|
| |
| // Function to collect form data from dynamic fields
| |
| function collectFormData() {
| |
| var formData = {};
| |
|
| |
| // Only collect dynamic field values (PAGE_NAME is handled separately)
| |
| $('.dynamic-field').each(function() {
| |
| var $field = $(this);
| |
| var fieldKey = $field.data('field-key');
| |
| var value = $field.val().trim();
| |
|
| |
| if (value) {
| |
| formData[fieldKey] = value;
| |
| }
| |
| });
| |
|
| |
| return formData;
| |
| }
| |
|
| |
| // Function to process $VARIABLE$ placeholders
| |
| function processPlaceholders(templateContent, pageName, formData) {
| |
| var processedContent = templateContent;
| |
|
| |
| // Replace $PAGE_NAME$ with the actual page name
| |
| if (pageName) {
| |
| processedContent = processedContent.replace(/\$PAGE_NAME\$/g, pageName);
| |
| }
| |
|
| |
| // Replace other placeholders with form data
| |
| for (var key in formData) {
| |
| if (key !== 'PAGE_NAME' && formData[key]) {
| |
| var placeholder = '$' + key + '$';
| |
| // Properly escape the $ characters for regex
| |
| var escapedPlaceholder = placeholder.replace(/\$/g, '\\$');
| |
| processedContent = processedContent.replace(new RegExp(escapedPlaceholder, 'g'), formData[key]);
| |
| }
| |
| }
| |
| return processedContent;
| |
| }
| |
|
| |
| // Handle create button click
| |
| $('#create-page-btn').on('click', function() {
| |
| var templateType = $('#template-type').val();
| |
| var pageName = $('#page-name').val().trim();
| |
|
| |
| if (!templateType || !pageName) {
| |
| alert('Please select a template type and enter a page name');
| |
| return;
| |
| }
| |
|
| |
| // Disable button to prevent double-clicks
| |
| $(this).prop('disabled', true).text('Creating...');
| |
|
| |
| fetchTemplateContent(templateType, function(templateContent) {
| |
| var formData = collectFormData();
| |
|
| |
| try {
| |
| var processedContent = cleanupUnfilledPlaceholders(templateContent, pageName, formData);
| |
| } catch (error) {
| |
| alert('Error: ' + error.message);
| |
| $('#create-page-btn').prop('disabled', false).text('Create Page');
| |
| return;
| |
| }
| |
|
| |
| var api = new mw.Api();
| |
| api.create(pageName, { summary: 'Creating new ' + templateType + ' page' }, processedContent)
| |
| .done(function() {
| |
| window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
| |
| })
| |
| .fail(function(code, error) {
| |
| if (code === 'articleexists') {
| |
| if (confirm('Page already exists. Do you want to edit it and add the template?')) {
| |
| window.location.href = mw.util.getUrl(pageName, { veaction: 'edit' });
| |
| }
| |
| } else {
| |
| console.error('Failed to create page:', code, error);
| |
| alert('Error creating page: ' + (error && error.error ? error.error.info : code));
| |
| }
| |
| $('#create-page-btn').prop('disabled', false).text('Create Page');
| |
| });
| |
| }, function(error) {
| |
| console.error('Failed to get template:', error);
| |
| alert('Error loading template. Please try again.');
| |
| $('#create-page-btn').prop('disabled', false).text('Create Page');
| |
| });
| |
| });
| |
|
| |
| console.log('TemplateStarter initialized');
| |
| });
| |
| }); | | }); |