Jump to content

MediaWiki:Common.js: Difference between revisions

From ZelocoreCMS Wiki
ZelocoreCMS JS enhancements — initial deploy
 
Change editintro from Template:Create_Intro to ZelocoreCMS:Style_Guide
 
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
/* ============================================================
/* ════════════════════════════════════════════════════════════════════
   ZelocoreCMS Wiki — MediaWiki:Common.js
   ZelocoreCMS Wiki — Enhanced Common.js (v2.0 world-class upgrade)
   Animations, UX Enhancements, Interactive Features
   Features: dark mode, animated counters, helpful widget,
   ============================================================ */
            keyboard shortcuts, reading progress, back-to-top
 
   ════════════════════════════════════════════════════════════════════ */
( function () {
( function () {
     'use strict';
     'use strict';


     /* ── Utility ─────────────────────────────────────────── */
     var qs  = function ( s, ctx ) { return ( ctx || document ).querySelector( s ); };
    function qs( sel, ctx ) { return ( ctx || document ).querySelector( sel ); }
     var qsa = function ( s, ctx ) { return Array.from( ( ctx || document ).querySelectorAll( s ) ); };
     function qsa( sel, ctx ) { return [ ...( ctx || document ).querySelectorAll( sel ) ]; }
 
     function ready( fn )     { if ( document.readyState !== 'loading' ) fn(); else document.addEventListener( 'DOMContentLoaded', fn ); }
     function ready( fn ) {
        if ( document.readyState !== 'loading' ) { fn(); }
        else { document.addEventListener( 'DOMContentLoaded', fn ); }
    }


     /* ── 1. Scroll-triggered Fade-in Animations ──────────── */
     /* ── Reading progress bar ─────────────────────────── */
     function initScrollAnimations() {
     function initProgress() {
         var observer = new IntersectionObserver( function( entries ) {
         var bar = document.createElement( 'div' );
             entries.forEach( function( entry ) {
        bar.id = 'cms-progress';
                if ( entry.isIntersecting ) {
        document.body.prepend( bar );
                    entry.target.style.opacity    = '1';
        window.addEventListener( 'scroll', function () {
                    entry.target.style.transform  = 'translateY(0)';
             var h = document.documentElement;
                    observer.unobserve( entry.target );
            var pct = ( h.scrollTop / ( h.scrollHeight - h.clientHeight ) ) * 100;
                }
            bar.style.width = Math.min( pct, 100 ) + '%';
            });
        }, { passive: true } );
         }, { threshold: 0.12 } );
    }
 
    /* ── Back to top ──────────────────────────────────── */
    function initBackTop() {
        var btn = document.createElement( 'button' );
        btn.id = 'cms-back-top';
        btn.title = 'Back to top';
        btn.innerHTML = '';
        document.body.appendChild( btn );
        window.addEventListener( 'scroll', function () {
            btn.classList.toggle( 'visible', window.scrollY > 400 );
        }, { passive: true } );
        btn.addEventListener( 'click', function () {
            window.scrollTo( { top: 0, behavior: 'smooth' } );
        } );
    }
 
    /* ── Dark mode toggle ─────────────────────────────── */
    function initDarkMode() {
        var btn = document.createElement( 'button' );
         btn.id = 'cms-dark-toggle';
        btn.title = 'Toggle dark mode';
        btn.innerHTML = '🌙';
        document.body.appendChild( btn );
 
        var isDark = localStorage.getItem( 'cms-dark' ) === '1';
        if ( isDark ) { document.body.classList.add( 'cms-dark-mode' ); btn.innerHTML = '☀️'; }


         qsa( '.zc-cms-card, .zc-category-box, .zc-stat-card, .zc-profile-card, .wikitable, h2, h3' )
         btn.addEventListener( 'click', function () {
             .forEach( function( el ) {
            isDark = !isDark;
                el.style.opacity    = '0';
            document.body.classList.toggle( 'cms-dark-mode', isDark );
                el.style.transform  = 'translateY(24px)';
             btn.innerHTML = isDark ? '☀️' : '🌙';
                el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
            localStorage.setItem( 'cms-dark', isDark ? '1' : '0' );
                observer.observe( el );
        } );
            });
     }
     }


     /* ── 2. Animated Counter Numbers ─────────────────────── */
     /* ── Animated number counters (main page stats) ───── */
     function animateCounter( el, target, duration ) {
     function animateCounter( el, target ) {
         var start     = 0;
         var start = 0;
        var duration = 1400;
         var startTime = null;
         var startTime = null;
        var suffix    = el.dataset.suffix || '';
         function step( ts ) {
 
             if ( !startTime ) { startTime = ts; }
         function step( timestamp ) {
             var progress = Math.min( ( ts - startTime ) / duration, 1 );
             if ( !startTime ) startTime = timestamp;
             var ease = 1 - Math.pow( 1 - progress, 3 );
             var progress = Math.min( ( timestamp - startTime ) / duration, 1 );
             el.textContent = Math.floor( ease * target ).toLocaleString();
             var ease     = 1 - Math.pow( 1 - progress, 3 ); // ease-out cubic
             if ( progress < 1 ) { requestAnimationFrame( step ); }
             el.textContent = Math.floor( ease * target ).toLocaleString() + suffix;
            else { el.textContent = target.toLocaleString(); }
             if ( progress < 1 ) requestAnimationFrame( step );
         }
         }
         requestAnimationFrame( step );
         requestAnimationFrame( step );
Line 50: Line 78:


     function initCounters() {
     function initCounters() {
         var counterObs = new IntersectionObserver( function( entries ) {
         qsa( '.mp-stat-num' ).forEach( function ( el ) {
             entries.forEach( function( entry ) {
             var raw = el.textContent.replace( /,/g, '' );
                if ( entry.isIntersecting ) {
            var num = parseInt( raw, 10 );
                    var el = entry.target;
            if ( !isNaN( num ) && num > 0 ) {
                    var val = parseInt( el.dataset.count, 10 );
                var obs = new IntersectionObserver( function ( entries, o ) {
                     animateCounter( el, val, 1800 );
                    if ( entries[0].isIntersecting ) {
                    counterObs.unobserve( el );
                        animateCounter( el, num );
                }
                        o.disconnect();
            });
                     }
         }, { threshold: 0.5 } );
                }, { threshold: 0.3 } );
                obs.observe( el );
            }
        } );
    }
 
    /* ── "Was this helpful?" widget ───────────────────── */
    function initHelpfulWidget() {
        var body = qs( '#mw-content-text .mw-parser-output' );
         if ( !body ) { return; }
        // Only on article pages, not main page
        if ( document.body.classList.contains( 'page-Main_Page' ) ) { return; }
        if ( !mw.config.get( 'wgIsArticle' ) ) { return; }
 
        var widget = document.createElement( 'div' );
        widget.id  = 'cms-helpful';
        widget.innerHTML = [
            '<span id="cms-helpful-label">Was this article helpful?</span>',
            '<button class="cms-helpful-btn yes" data-v="yes">👍 Yes</button>',
            '<button class="cms-helpful-btn no"  data-v="no">👎 No</button>',
            '<span id="cms-helpful-thanks">Thanks for your feedback! 🎉</span>'
        ].join( '' );
        body.appendChild( widget );


         qsa( '.zc-count' ).forEach( function( el ) { counterObs.observe( el ); } );
         qsa( '.cms-helpful-btn', widget ).forEach( function ( btn ) {
            btn.addEventListener( 'click', function () {
                qsa( '.cms-helpful-btn', widget ).forEach( function ( b ) { b.classList.remove( 'active' ); } );
                btn.classList.add( 'active' );
                qs( '#cms-helpful-thanks' ).style.display = 'inline';
            } );
        } );
     }
     }


     /* ── 3. Enhanced Search Bar ──────────────────────────── */
     /* ── Keyboard shortcut: Ctrl+K = focus search ─────── */
     function initSearch() {
     function initSearchShortcut() {
        var input = qs( '#searchInput, .cdx-text-input__input, input[name="search"]' );
         document.addEventListener( 'keydown', function ( e ) {
        if ( !input ) return;
 
        // Quick-search shortcuts (Ctrl+K or /)
         document.addEventListener( 'keydown', function( e ) {
             if ( ( e.ctrlKey || e.metaKey ) && e.key === 'k' ) {
             if ( ( e.ctrlKey || e.metaKey ) && e.key === 'k' ) {
                 e.preventDefault();
                 e.preventDefault();
                 input.focus();
                 var input = qs( '#searchInput, .cdx-text-input__input' );
                 input.select();
                 if ( input ) { input.focus(); input.select(); }
            }
            if ( e.key === '/' && document.activeElement.tagName !== 'INPUT' &&
                document.activeElement.tagName !== 'TEXTAREA' ) {
                e.preventDefault();
                input.focus();
             }
             }
         } );
         } );
    }


        // Keyboard shortcut hint
    /* ── Copy buttons on <pre> blocks ─────────────────── */
         if ( !qs( '.zc-search-hint' ) ) {
    function addCopyButtons() {
             var hint = document.createElement( 'span' );
         qsa( 'pre' ).forEach( function ( pre ) {
             hint.className = 'zc-search-hint';
            if ( qs( '.cms-copy-btn', pre ) ) { return; }
             hint.textContent = 'Ctrl+K';
             var btn = document.createElement( 'button' );
             hint.style.cssText = [
             btn.className = 'cms-copy-btn';
                'position:absolute', 'right:10px', 'top:50%', 'transform:translateY(-50%)',
             btn.textContent = 'Copy';
                'font-size:0.7rem', 'color:var(--zc-text-dim)',
             btn.style.cssText = 'position:absolute;top:8px;right:10px;padding:3px 10px;' +
                'background:var(--zc-surface2)', 'border:1px solid var(--zc-border)',
                'background:rgba(255,255,255,.15);color:#e2e8f0;border:1px solid rgba(255,255,255,.25);' +
                 'border-radius:4px', 'padding:1px 6px', 'pointer-events:none'
                 'border-radius:4px;font-size:.78em;cursor:pointer;transition:background .2s;';
             ].join(';');
             btn.addEventListener( 'mouseenter', function () { btn.style.background = 'rgba(255,255,255,.28)'; } );
             var wrap = input.closest( '.cdx-text-input, .mw-search-form, .vector-search-box' );
             btn.addEventListener( 'mouseleave', function () { btn.style.background = 'rgba(255,255,255,.15)'; } );
             if ( wrap ) {
             btn.addEventListener( 'click', function () {
                 wrap.style.position = 'relative';
                navigator.clipboard.writeText( pre.innerText ).then( function () {
                wrap.appendChild( hint );
                    btn.textContent = '✅ Copied!';
            }
                    setTimeout( function () { btn.textContent = 'Copy'; }, 2000 );
         }
                 } );
            } );
            pre.style.position = 'relative';
            pre.appendChild( btn );
         } );
     }
     }


     /* ── 4. Active Sidebar Link Highlight ────────────────── */
     /* ── Scroll reveal for portal cells ──────────────── */
     function initSidebarHighlight() {
     function initScrollReveal() {
         var currentPath = window.location.pathname;
         if ( !window.IntersectionObserver ) { return; }
         qsa( '#mw-panel a, .mw-sidebar a, .vector-menu-content-list a' ).forEach( function( a ) {
         var obs = new IntersectionObserver( function ( entries ) {
            if ( a.pathname && a.pathname !== '/' && currentPath.startsWith( a.pathname ) ) {
            entries.forEach( function ( entry ) {
                a.style.color      = 'var(--zc-cyan)';
                if ( entry.isIntersecting ) {
                a.style.borderLeft  = '2px solid var(--zc-cyan)';
                    entry.target.style.opacity  = '1';
                a.style.background  = 'var(--zc-cyan-dim)';
                    entry.target.style.transform = 'translateY(0)';
                a.style.fontWeight  = '600';
                    obs.unobserve( entry.target );
             }
                }
         });
            } );
        }, { threshold: 0.06 } );
        qsa( '.mp-portal-cell, .mp-cat-card, .mp-card, .mp-stat' ).forEach( function ( el ) {
            el.style.opacity  = '0';
            el.style.transform = 'translateY(14px)';
            el.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
             obs.observe( el );
         } );
     }
     }


     /* ── 5. Code Block Copy Button ───────────────────────── */
     /* ── Table row highlight ──────────────────────────── */
     function initCodeCopy() {
     function initTableHighlight() {
         qsa( 'pre, .mw-highlight' ).forEach( function( block ) {
         qsa( '.wikitable tbody tr' ).forEach( function ( row ) {
             if ( qs( '.zc-copy-btn', block ) ) return;
             row.style.transition = 'background .15s';
 
         } );
            var btn = document.createElement( 'button' );
            btn.className  = 'zc-copy-btn';
            btn.textContent = '⎘ Copy';
            btn.style.cssText = [
                'position:absolute', 'top:10px', 'right:10px',
                'background:var(--zc-surface2)', 'border:1px solid var(--zc-border)',
                'color:var(--zc-text-muted)', 'font-size:0.75rem', 'font-weight:600',
                'padding:3px 10px', 'border-radius:4px', 'cursor:pointer',
                'transition:all 0.2s ease', 'font-family:var(--zc-font-body)'
            ].join(';');
 
            btn.addEventListener( 'mouseenter', function() {
                this.style.borderColor = 'var(--zc-cyan)';
                this.style.color      = 'var(--zc-cyan)';
            });
            btn.addEventListener( 'mouseleave', function() {
                this.style.borderColor = 'var(--zc-border)';
                this.style.color      = 'var(--zc-text-muted)';
            });
            btn.addEventListener( 'click', function() {
                var text = block.textContent.replace( /⎘ Copy|✓ Copied/g, '' ).trim();
                navigator.clipboard.writeText( text ).then( function() {
                    btn.textContent      = '✓ Copied';
                    btn.style.color      = 'var(--zc-emerald)';
                    btn.style.borderColor = 'var(--zc-emerald)';
                    setTimeout( function() {
                        btn.textContent      = '⎘ Copy';
                        btn.style.color      = 'var(--zc-text-muted)';
                        btn.style.borderColor = 'var(--zc-border)';
                    }, 2000 );
                });
            });
 
            block.style.position = 'relative';
            block.appendChild( btn );
         });
     }
     }


     /* ── 6. Smooth Anchor Scrolling with Offset ──────────── */
     /* ── External link icon ───────────────────────────── */
     function initSmoothScroll() {
     function initExternalLinks() {
         qsa( 'a[href^="#"]' ).forEach( function( a ) {
         qsa( '.mw-body-content a.external' ).forEach( function ( a ) {
             a.addEventListener( 'click', function( e ) {
             if ( !qs( '.cms-ext', a ) ) {
                 var target = qs( decodeURIComponent( this.hash ) );
                 var s = document.createElement( 'span' );
                 if ( !target ) return;
                 s.className = 'cms-ext';
                 e.preventDefault();
                 s.setAttribute( 'aria-hidden', 'true' );
                 window.scrollTo( {
                 s.textContent = ' ↗';
                    top:      target.getBoundingClientRect().top + window.scrollY - 80,
                s.style.cssText = 'font-size:.72em;color:#7c3aed;margin-left:1px;';
                    behavior: 'smooth'
                 a.appendChild( s );
                 } );
             }
             });
         } );
         });
     }
     }


     /* ── 7. TOC Sticky Scroll Spy ────────────────────────── */
     /* ── Bootstrap ────────────────────────────────────── */
     function initTOCSpy() {
     ready( function () {
         var toc = qs( '#toc, .toc' );
         initProgress();
         if ( !toc ) return;
        initBackTop();
        initDarkMode();
        initCounters();
        initSearchShortcut();
        initScrollReveal();
        initTableHighlight();
        initExternalLinks();
        setTimeout( addCopyButtons, 500 );
         setTimeout( initHelpfulWidget, 300 );


         var headings = qsa( '.mw-body-content h2, .mw-body-content h3' );
         if ( mw && mw.hook ) {
        var links    = qsa( '#toc a, .toc a' );
            mw.hook( 've.deactivate' ).add( function () {
         if ( !headings.length || !links.length ) return;
                setTimeout( addCopyButtons, 600 );
            } );
         }
    } );


        window.addEventListener( 'scroll', mw.util.debounce( function() {
}() );
            var scrollY = window.scrollY + 100;
            var current = '';


            headings.forEach( function( h ) {
// --- Strict Create Article App ---
                 if ( h.offsetTop <= scrollY ) current = '#' + h.id;
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function() {
    $(function() {
        var $app = $('#strict-create-app');
        if ($app.length) {
            var categories = [
                'Blogging_CMS', 'Headless_CMS', 'ECommerce_CMS',
                'Enterprise_CMS', 'Open_Source_CMS', 'SaaS_CMS', 'Wiki_Platforms'
            ];
           
            var html = '<div class="strict-creator">' +
                '<label class="sc-main-label">1. Platform Name</label>' +
                '<input type="text" id="sc-title" placeholder="e.g. Contentful" autocomplete="off">' +
                '<div id="sc-status"></div>' +
                 '<label class="sc-main-label">2. Select Categories (1 to 5)</label>' +
                '<div id="sc-cats"></div>' +
                '<button id="sc-btn" disabled>📝 Continue to Editor</button>' +
                '<div style="text-align:center;margin-top:8px;font-size:0.85em;"><a href="' + mw.util.getUrl('ZelocoreCMS:Style_Guide') + '">Read article style guide</a></div>' +
                '</div>';
            $app.html(html);
           
            var $cats = $('#sc-cats');
            categories.forEach(function(cat) {
                $cats.append('<label class="sc-cat-label"><input type="checkbox" value="'+cat+'"> '+cat.replace(/_/g, ' ')+'</label>');
             });
             });
 
           
             links.forEach( function( link ) {
            var $title = $('#sc-title');
                 link.style.color      = 'var(--zc-text-muted)';
            var $status = $('#sc-status');
                link.style.fontWeight = '400';
             var $btn = $('#sc-btn');
                 if ( link.getAttribute( 'href' ) === current ) {
            var $checkboxes = $cats.find('input[type="checkbox"]');
                     link.style.color      = 'var(--zc-cyan)';
           
                     link.style.fontWeight = '600';
            var isTitleValid = false;
            var debounceTimer;
           
            function updateBtn() {
                 var checkedCount = $checkboxes.filter(':checked').length;
               
                // Handle styling and max limits
                $checkboxes.each(function() {
                    var $lbl = $(this).parent();
                    if ($(this).is(':checked')) {
                        $lbl.addClass('is-checked').removeClass('is-disabled');
                        $(this).prop('disabled', false);
                    } else {
                        $lbl.removeClass('is-checked');
                        if (checkedCount >= 5) {
                            $lbl.addClass('is-disabled');
                            $(this).prop('disabled', true);
                        } else {
                            $lbl.removeClass('is-disabled');
                            $(this).prop('disabled', false);
                        }
                    }
                });
               
                if (isTitleValid && checkedCount > 0 && checkedCount <= 5) {
                    $btn.prop('disabled', false);
                 } else {
                    $btn.prop('disabled', true);
                }
            }
           
            $checkboxes.on('change', updateBtn);
           
            $title.on('input', function() {
                var val = $(this).val().trim();
                clearTimeout(debounceTimer);
                isTitleValid = false;
                updateBtn();
               
                if (val.length === 0) {
                     $status.html('');
                     $title.css('border-color', '#cbd5e1');
                    return;
                 }
                 }
               
                $status.html('⏳ Checking for exact matches...').css('color', '#72777d');
                $title.css('border-color', '#cbd5e1');
               
                debounceTimer = setTimeout(function() {
                    var api = new mw.Api();
                    // Use search API which is case-insensitive
                    api.get({
                        action: 'query',
                        list: 'search',
                        srsearch: val,
                        format: 'json'
                    }).done(function(data) {
                        var results = data.query.search;
                        var exactMatch = null;
                       
                        // Strict case-insensitive check against all results
                        for (var i = 0; i < results.length; i++) {
                            if (results[i].title.toLowerCase() === val.toLowerCase()) {
                                exactMatch = results[i].title;
                                break;
                            }
                        }
                       
                        if (exactMatch) {
                            $status.html('⚠️ Already exists as "<b>' + exactMatch + '</b>". <a href="' + mw.util.getUrl(exactMatch) + '">Edit it</a>').css('color', '#d33');
                            $title.css('border-color', '#d33');
                        } else {
                            isTitleValid = true;
                            $status.html('✅ Available!').css('color', '#00af89');
                            $title.css('border-color', '#00af89');
                            updateBtn();
                        }
                    }).fail(function() {
                        $status.html('❌ API error').css('color', '#d33');
                    });
                }, 500);
             });
             });
        }, 100 ) );
           
    }
            $btn.on('click', function() {
 
                var finalTitle = $title.val().trim();
    /* ── 8. Developer Profile Tab Switcher ───────────────── */
                finalTitle = finalTitle.charAt(0).toUpperCase() + finalTitle.slice(1);
    function initProfileTabs() {
               
        qsa( '.zc-tabs' ).forEach( function( tabGroup ) {
                var selectedCats = [];
            var tabs    = qsa( '.zc-tab-btn', tabGroup );
                 $checkboxes.filter(':checked').each(function() {
            var panels  = qsa( '.zc-tab-panel', tabGroup );
                     selectedCats.push($(this).val());
 
                });
            tabs.forEach( function( tab, i ) {
               
                 tab.addEventListener( 'click', function() {
                var url = mw.util.getUrl(finalTitle, {
                     tabs.forEach( function( t ) {
                     action: 'edit',
                        t.style.color      = 'var(--zc-text-muted)';
                     preload: 'Template:New_CMS_Draft',
                        t.style.borderColor = 'transparent';
                     editintro: 'ZelocoreCMS:Style_Guide',
                        t.style.background  = 'transparent';
                     prefill_cats: selectedCats.join(',')
                    });
                    panels.forEach( function( p ) { p.style.display = 'none'; } );
 
                     tab.style.color      = 'var(--zc-cyan)';
                     tab.style.borderColor = 'var(--zc-cyan)';
                     tab.style.background  = 'var(--zc-cyan-dim)';
                     if ( panels[ i ] ) panels[ i ].style.display = 'block';
                 });
                 });
                window.location.href = url;
             });
             });
 
         }
            if ( tabs[ 0 ] ) tabs[ 0 ].click();
       
         });
        // --- Edit Page Category Injector ---
    }
         if (mw.config.get('wgAction') === 'edit') {
 
            var urlParams = new URLSearchParams(window.location.search);
    /* ── 9. Verified Badge Glow Pulse on Hover ───────────── */
             var cats = urlParams.get('prefill_cats');
    function initVerifiedHover() {
             if (cats) {
         qsa( '.zc-verified-banner' ).forEach( function( el ) {
                // Wait briefly for textarea to be ready
            el.addEventListener( 'mouseenter', function() {
                setTimeout(function() {
                this.style.boxShadow = '0 0 40px rgba(0,232,143,0.5)';
                    var $textbox = $('#wpTextbox1');
             });
                    if ($textbox.length) {
            el.addEventListener( 'mouseleave', function() {
                        var currentVal = $textbox.val();
                this.style.boxShadow = '';
                        // Prevent duplicate injections on refresh
             });
                        if (currentVal.indexOf('[[Category:CMS_Platforms]]') === -1) {
        });
                            var catString = '\n\n[[Category:CMS_Platforms]]\n';
    }
                            var catArray = cats.split(',');
 
                            catArray.forEach(function(c) {
    /* ── 10. Reading Progress Bar ────────────────────────── */
                                catString += '[[Category:' + c + ']]\n';
    function initProgressBar() {
                            });
        var bar = document.createElement( 'div' );
                            $textbox.val(currentVal.trim() + catString);
        bar.id = 'zc-reading-progress';
                        }
        bar.style.cssText = [
                       
            'position:fixed', 'top:0', 'left:0', 'height:2px', 'width:0%',
                        // Clean the URL so refresh doesn't trigger it again
            'background:linear-gradient(90deg, var(--zc-cyan), var(--zc-violet))',
                        var cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?title=' + encodeURIComponent(mw.config.get('wgPageName')) + '&action=edit';
            'z-index:9999', 'transition:width 0.1s linear',
                        window.history.replaceState({path:cleanUrl}, '', cleanUrl);
            'box-shadow:0 0 8px rgba(0,200,255,0.6)'
                    }
        ].join(';');
                }, 500);
        document.body.appendChild( bar );
             }
 
         }
        window.addEventListener( 'scroll', function() {
            var docHeight    = document.documentElement.scrollHeight - window.innerHeight;
            var scrolled    = ( window.scrollY / docHeight ) * 100;
            bar.style.width  = Math.min( scrolled, 100 ) + '%';
        });
    }
 
    /* ── 11. Back-to-Top Button ──────────────────────────── */
    function initBackToTop() {
        var btn = document.createElement( 'button' );
        btn.id          = 'zc-back-to-top';
        btn.textContent = '↑';
        btn.title      = 'Back to top';
        btn.style.cssText = [
            'position:fixed', 'bottom:28px', 'right:28px',
            'width:42px', 'height:42px', 'border-radius:50%',
            'background:linear-gradient(135deg,var(--zc-cyan),var(--zc-violet))',
            'border:none', 'color:#000', 'font-size:1.2rem', 'font-weight:700',
            'cursor:pointer', 'opacity:0', 'transition:opacity 0.3s ease, transform 0.3s ease',
            'box-shadow:0 4px 20px rgba(0,200,255,0.4)', 'z-index:999'
        ].join(';');
 
        document.body.appendChild( btn );
 
        window.addEventListener( 'scroll', function() {
            btn.style.opacity  = window.scrollY > 400 ? '1' : '0';
            btn.style.transform = window.scrollY > 400 ? 'scale(1)' : 'scale(0.8)';
        });
 
        btn.addEventListener( 'click', function() {
            window.scrollTo( { top: 0, behavior: 'smooth' } );
        });
    }
 
    /* ── 12. Dark/Light mode toggle hint ────────────────── */
    // Wiki stays dark — but we persist the preference
    function initTheme() {
        document.documentElement.setAttribute( 'data-zc-theme', 'dark' );
    }
 
    /* ── Bootstrap ───────────────────────────────────────── */
    ready( function() {
        initTheme();
        initProgressBar();
        initBackToTop();
        initSearch();
        initSidebarHighlight();
        initScrollAnimations();
        initCounters();
        initSmoothScroll();
        initTOCSpy();
        initProfileTabs();
        initVerifiedHover();
 
        // Code copy needs a slight delay for SyntaxHighlight to render
        setTimeout( initCodeCopy, 500 );
 
        // Re-run code copy after VE switches back to read mode
        mw.hook( 've.deactivate' ).add( function() {
             setTimeout( initCodeCopy, 800 );
         });
     });
     });
 
});
}() );
// --- End Strict Create App ---

Latest revision as of 22:55, 29 July 2026

/* ════════════════════════════════════════════════════════════════════
   ZelocoreCMS Wiki — Enhanced Common.js  (v2.0 world-class upgrade)
   Features: dark mode, animated counters, helpful widget,
             keyboard shortcuts, reading progress, back-to-top
   ════════════════════════════════════════════════════════════════════ */
( function () {
    'use strict';

    var qs  = function ( s, ctx ) { return ( ctx || document ).querySelector( s ); };
    var qsa = function ( s, ctx ) { return Array.from( ( ctx || document ).querySelectorAll( s ) ); };

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

    /* ── Reading progress bar ─────────────────────────── */
    function initProgress() {
        var bar = document.createElement( 'div' );
        bar.id = 'cms-progress';
        document.body.prepend( bar );
        window.addEventListener( 'scroll', function () {
            var h = document.documentElement;
            var pct = ( h.scrollTop / ( h.scrollHeight - h.clientHeight ) ) * 100;
            bar.style.width = Math.min( pct, 100 ) + '%';
        }, { passive: true } );
    }

    /* ── Back to top ──────────────────────────────────── */
    function initBackTop() {
        var btn = document.createElement( 'button' );
        btn.id = 'cms-back-top';
        btn.title = 'Back to top';
        btn.innerHTML = '↑';
        document.body.appendChild( btn );
        window.addEventListener( 'scroll', function () {
            btn.classList.toggle( 'visible', window.scrollY > 400 );
        }, { passive: true } );
        btn.addEventListener( 'click', function () {
            window.scrollTo( { top: 0, behavior: 'smooth' } );
        } );
    }

    /* ── Dark mode toggle ─────────────────────────────── */
    function initDarkMode() {
        var btn = document.createElement( 'button' );
        btn.id = 'cms-dark-toggle';
        btn.title = 'Toggle dark mode';
        btn.innerHTML = '🌙';
        document.body.appendChild( btn );

        var isDark = localStorage.getItem( 'cms-dark' ) === '1';
        if ( isDark ) { document.body.classList.add( 'cms-dark-mode' ); btn.innerHTML = '☀️'; }

        btn.addEventListener( 'click', function () {
            isDark = !isDark;
            document.body.classList.toggle( 'cms-dark-mode', isDark );
            btn.innerHTML = isDark ? '☀️' : '🌙';
            localStorage.setItem( 'cms-dark', isDark ? '1' : '0' );
        } );
    }

    /* ── Animated number counters (main page stats) ───── */
    function animateCounter( el, target ) {
        var start = 0;
        var duration = 1400;
        var startTime = null;
        function step( ts ) {
            if ( !startTime ) { startTime = ts; }
            var progress = Math.min( ( ts - startTime ) / duration, 1 );
            var ease = 1 - Math.pow( 1 - progress, 3 );
            el.textContent = Math.floor( ease * target ).toLocaleString();
            if ( progress < 1 ) { requestAnimationFrame( step ); }
            else { el.textContent = target.toLocaleString(); }
        }
        requestAnimationFrame( step );
    }

    function initCounters() {
        qsa( '.mp-stat-num' ).forEach( function ( el ) {
            var raw = el.textContent.replace( /,/g, '' );
            var num = parseInt( raw, 10 );
            if ( !isNaN( num ) && num > 0 ) {
                var obs = new IntersectionObserver( function ( entries, o ) {
                    if ( entries[0].isIntersecting ) {
                        animateCounter( el, num );
                        o.disconnect();
                    }
                }, { threshold: 0.3 } );
                obs.observe( el );
            }
        } );
    }

    /* ── "Was this helpful?" widget ───────────────────── */
    function initHelpfulWidget() {
        var body = qs( '#mw-content-text .mw-parser-output' );
        if ( !body ) { return; }
        // Only on article pages, not main page
        if ( document.body.classList.contains( 'page-Main_Page' ) ) { return; }
        if ( !mw.config.get( 'wgIsArticle' ) ) { return; }

        var widget = document.createElement( 'div' );
        widget.id  = 'cms-helpful';
        widget.innerHTML = [
            '<span id="cms-helpful-label">Was this article helpful?</span>',
            '<button class="cms-helpful-btn yes" data-v="yes">👍 Yes</button>',
            '<button class="cms-helpful-btn no"  data-v="no">👎 No</button>',
            '<span id="cms-helpful-thanks">Thanks for your feedback! 🎉</span>'
        ].join( '' );
        body.appendChild( widget );

        qsa( '.cms-helpful-btn', widget ).forEach( function ( btn ) {
            btn.addEventListener( 'click', function () {
                qsa( '.cms-helpful-btn', widget ).forEach( function ( b ) { b.classList.remove( 'active' ); } );
                btn.classList.add( 'active' );
                qs( '#cms-helpful-thanks' ).style.display = 'inline';
            } );
        } );
    }

    /* ── Keyboard shortcut: Ctrl+K = focus search ─────── */
    function initSearchShortcut() {
        document.addEventListener( 'keydown', function ( e ) {
            if ( ( e.ctrlKey || e.metaKey ) && e.key === 'k' ) {
                e.preventDefault();
                var input = qs( '#searchInput, .cdx-text-input__input' );
                if ( input ) { input.focus(); input.select(); }
            }
        } );
    }

    /* ── Copy buttons on <pre> blocks ─────────────────── */
    function addCopyButtons() {
        qsa( 'pre' ).forEach( function ( pre ) {
            if ( qs( '.cms-copy-btn', pre ) ) { return; }
            var btn = document.createElement( 'button' );
            btn.className = 'cms-copy-btn';
            btn.textContent = 'Copy';
            btn.style.cssText = 'position:absolute;top:8px;right:10px;padding:3px 10px;' +
                'background:rgba(255,255,255,.15);color:#e2e8f0;border:1px solid rgba(255,255,255,.25);' +
                'border-radius:4px;font-size:.78em;cursor:pointer;transition:background .2s;';
            btn.addEventListener( 'mouseenter', function () { btn.style.background = 'rgba(255,255,255,.28)'; } );
            btn.addEventListener( 'mouseleave', function () { btn.style.background = 'rgba(255,255,255,.15)'; } );
            btn.addEventListener( 'click', function () {
                navigator.clipboard.writeText( pre.innerText ).then( function () {
                    btn.textContent = '✅ Copied!';
                    setTimeout( function () { btn.textContent = 'Copy'; }, 2000 );
                } );
            } );
            pre.style.position = 'relative';
            pre.appendChild( btn );
        } );
    }

    /* ── Scroll reveal for portal cells ──────────────── */
    function initScrollReveal() {
        if ( !window.IntersectionObserver ) { return; }
        var obs = new IntersectionObserver( function ( entries ) {
            entries.forEach( function ( entry ) {
                if ( entry.isIntersecting ) {
                    entry.target.style.opacity  = '1';
                    entry.target.style.transform = 'translateY(0)';
                    obs.unobserve( entry.target );
                }
            } );
        }, { threshold: 0.06 } );
        qsa( '.mp-portal-cell, .mp-cat-card, .mp-card, .mp-stat' ).forEach( function ( el ) {
            el.style.opacity   = '0';
            el.style.transform = 'translateY(14px)';
            el.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
            obs.observe( el );
        } );
    }

    /* ── Table row highlight ──────────────────────────── */
    function initTableHighlight() {
        qsa( '.wikitable tbody tr' ).forEach( function ( row ) {
            row.style.transition = 'background .15s';
        } );
    }

    /* ── External link icon ───────────────────────────── */
    function initExternalLinks() {
        qsa( '.mw-body-content a.external' ).forEach( function ( a ) {
            if ( !qs( '.cms-ext', a ) ) {
                var s = document.createElement( 'span' );
                s.className = 'cms-ext';
                s.setAttribute( 'aria-hidden', 'true' );
                s.textContent = ' ↗';
                s.style.cssText = 'font-size:.72em;color:#7c3aed;margin-left:1px;';
                a.appendChild( s );
            }
        } );
    }

    /* ── Bootstrap ────────────────────────────────────── */
    ready( function () {
        initProgress();
        initBackTop();
        initDarkMode();
        initCounters();
        initSearchShortcut();
        initScrollReveal();
        initTableHighlight();
        initExternalLinks();
        setTimeout( addCopyButtons, 500 );
        setTimeout( initHelpfulWidget, 300 );

        if ( mw && mw.hook ) {
            mw.hook( 've.deactivate' ).add( function () {
                setTimeout( addCopyButtons, 600 );
            } );
        }
    } );

}() );

// --- Strict Create Article App ---
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function() {
    $(function() {
        var $app = $('#strict-create-app');
        if ($app.length) {
            var categories = [
                'Blogging_CMS', 'Headless_CMS', 'ECommerce_CMS', 
                'Enterprise_CMS', 'Open_Source_CMS', 'SaaS_CMS', 'Wiki_Platforms'
            ];
            
            var html = '<div class="strict-creator">' +
                '<label class="sc-main-label">1. Platform Name</label>' +
                '<input type="text" id="sc-title" placeholder="e.g. Contentful" autocomplete="off">' +
                '<div id="sc-status"></div>' +
                '<label class="sc-main-label">2. Select Categories (1 to 5)</label>' +
                '<div id="sc-cats"></div>' +
                '<button id="sc-btn" disabled>📝 Continue to Editor</button>' +
                '<div style="text-align:center;margin-top:8px;font-size:0.85em;"><a href="' + mw.util.getUrl('ZelocoreCMS:Style_Guide') + '">Read article style guide</a></div>' +
                '</div>';
            $app.html(html);
            
            var $cats = $('#sc-cats');
            categories.forEach(function(cat) {
                $cats.append('<label class="sc-cat-label"><input type="checkbox" value="'+cat+'"> '+cat.replace(/_/g, ' ')+'</label>');
            });
            
            var $title = $('#sc-title');
            var $status = $('#sc-status');
            var $btn = $('#sc-btn');
            var $checkboxes = $cats.find('input[type="checkbox"]');
            
            var isTitleValid = false;
            var debounceTimer;
            
            function updateBtn() {
                var checkedCount = $checkboxes.filter(':checked').length;
                
                // Handle styling and max limits
                $checkboxes.each(function() {
                    var $lbl = $(this).parent();
                    if ($(this).is(':checked')) {
                        $lbl.addClass('is-checked').removeClass('is-disabled');
                        $(this).prop('disabled', false);
                    } else {
                        $lbl.removeClass('is-checked');
                        if (checkedCount >= 5) {
                            $lbl.addClass('is-disabled');
                            $(this).prop('disabled', true);
                        } else {
                            $lbl.removeClass('is-disabled');
                            $(this).prop('disabled', false);
                        }
                    }
                });
                
                if (isTitleValid && checkedCount > 0 && checkedCount <= 5) {
                    $btn.prop('disabled', false);
                } else {
                    $btn.prop('disabled', true);
                }
            }
            
            $checkboxes.on('change', updateBtn);
            
            $title.on('input', function() {
                var val = $(this).val().trim();
                clearTimeout(debounceTimer);
                isTitleValid = false;
                updateBtn();
                
                if (val.length === 0) {
                    $status.html('');
                    $title.css('border-color', '#cbd5e1');
                    return;
                }
                
                $status.html('⏳ Checking for exact matches...').css('color', '#72777d');
                $title.css('border-color', '#cbd5e1');
                
                debounceTimer = setTimeout(function() {
                    var api = new mw.Api();
                    // Use search API which is case-insensitive
                    api.get({
                        action: 'query',
                        list: 'search',
                        srsearch: val,
                        format: 'json'
                    }).done(function(data) {
                        var results = data.query.search;
                        var exactMatch = null;
                        
                        // Strict case-insensitive check against all results
                        for (var i = 0; i < results.length; i++) {
                            if (results[i].title.toLowerCase() === val.toLowerCase()) {
                                exactMatch = results[i].title;
                                break;
                            }
                        }
                        
                        if (exactMatch) {
                            $status.html('⚠️ Already exists as "<b>' + exactMatch + '</b>". <a href="' + mw.util.getUrl(exactMatch) + '">Edit it</a>').css('color', '#d33');
                            $title.css('border-color', '#d33');
                        } else {
                            isTitleValid = true;
                            $status.html('✅ Available!').css('color', '#00af89');
                            $title.css('border-color', '#00af89');
                            updateBtn();
                        }
                    }).fail(function() {
                        $status.html('❌ API error').css('color', '#d33');
                    });
                }, 500);
            });
            
            $btn.on('click', function() {
                var finalTitle = $title.val().trim();
                finalTitle = finalTitle.charAt(0).toUpperCase() + finalTitle.slice(1);
                
                var selectedCats = [];
                $checkboxes.filter(':checked').each(function() {
                    selectedCats.push($(this).val());
                });
                
                var url = mw.util.getUrl(finalTitle, {
                    action: 'edit',
                    preload: 'Template:New_CMS_Draft',
                    editintro: 'ZelocoreCMS:Style_Guide',
                    prefill_cats: selectedCats.join(',')
                });
                window.location.href = url;
            });
        }
        
        // --- Edit Page Category Injector ---
        if (mw.config.get('wgAction') === 'edit') {
            var urlParams = new URLSearchParams(window.location.search);
            var cats = urlParams.get('prefill_cats');
            if (cats) {
                // Wait briefly for textarea to be ready
                setTimeout(function() {
                    var $textbox = $('#wpTextbox1');
                    if ($textbox.length) {
                        var currentVal = $textbox.val();
                        // Prevent duplicate injections on refresh
                        if (currentVal.indexOf('[[Category:CMS_Platforms]]') === -1) {
                            var catString = '\n\n[[Category:CMS_Platforms]]\n';
                            var catArray = cats.split(',');
                            catArray.forEach(function(c) {
                                catString += '[[Category:' + c + ']]\n';
                            });
                            $textbox.val(currentVal.trim() + catString);
                        }
                        
                        // Clean the URL so refresh doesn't trigger it again
                        var cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?title=' + encodeURIComponent(mw.config.get('wgPageName')) + '&action=edit';
                        window.history.replaceState({path:cleanUrl}, '', cleanUrl);
                    }
                }, 500);
            }
        }
    });
});
// --- End Strict Create App ---