MediaWiki:Gadget-wix-core.js

Revision as of 13:40, 31 March 2026 by Wikilah admin (talk | contribs) (Created page with "/* ================================================================ WIX-CORE.JS — Wiki Interactive Experience: Shared Foundation ================================================================ Loaded via ResourceLoader on every page (hidden gadget). Provides DOM helpers, state management, event delegation, and UI-building utilities used by all wix-* category gadgets. Exposes: window.wix (single global namespace) RULES: - All public API live...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* ================================================================
   WIX-CORE.JS — Wiki Interactive Experience: Shared Foundation
   ================================================================
   Loaded via ResourceLoader on every page (hidden gadget).
   Provides DOM helpers, state management, event delegation, and
   UI-building utilities used by all wix-* category gadgets.

   Exposes:  window.wix  (single global namespace)

   RULES:
   - All public API lives under window.wix.
   - Never touch the DOM outside [data-wix-module] containers.
   - Guard every hook callback so it costs nothing on pages
     without WIX content.
   ================================================================ */

( function () {
  'use strict';

  // Prevent double-load
  if ( window.wix ) {
    return;
  }

  /* ── Namespace ────────────────────────────────────────────────── */

  var wix = {};


  /* ── 1. DOM Helpers ───────────────────────────────────────────── */

  /**
   * Shorthand for querySelector scoped to a parent.
   * @param {string} selector
   * @param {Element} [parent=document]
   * @returns {Element|null}
   */
  wix.qs = function ( selector, parent ) {
    return ( parent || document ).querySelector( selector );
  };

  /**
   * Shorthand for querySelectorAll, returns a real Array.
   * @param {string} selector
   * @param {Element} [parent=document]
   * @returns {Element[]}
   */
  wix.qsa = function ( selector, parent ) {
    return Array.prototype.slice.call(
      ( parent || document ).querySelectorAll( selector )
    );
  };

  /**
   * Create an element with optional attributes, classes, and children.
   *
   *   wix.el('div', { className: 'wix-card', 'data-idx': '3' }, [
   *     wix.el('span', { className: 'wix-badge', textContent: '1' }),
   *     'Plain text node'
   *   ]);
   *
   * @param {string} tag
   * @param {Object} [attrs]  - Applied via setAttribute, except:
   *        className  -> el.className
   *        textContent -> el.textContent
   *        innerHTML  -> el.innerHTML
   *        style (object) -> Object.assign(el.style, ...)
   * @param {Array<Element|string>} [children]
   * @returns {Element}
   */
  wix.el = function ( tag, attrs, children ) {
    var el = document.createElement( tag );
    var directProps = { className: 1, textContent: 1, innerHTML: 1 };

    if ( attrs ) {
      Object.keys( attrs ).forEach( function ( key ) {
        if ( directProps[ key ] ) {
          el[ key ] = attrs[ key ];
        } else if ( key === 'style' && typeof attrs[ key ] === 'object' ) {
          Object.keys( attrs[ key ] ).forEach( function ( prop ) {
            el.style[ prop ] = attrs[ key ][ prop ];
          } );
        } else {
          el.setAttribute( key, attrs[ key ] );
        }
      } );
    }

    if ( children ) {
      children.forEach( function ( child ) {
        if ( child == null ) {
          return;
        }
        if ( typeof child === 'string' || typeof child === 'number' ) {
          el.appendChild( document.createTextNode( String( child ) ) );
        } else {
          el.appendChild( child );
        }
      } );
    }

    return el;
  };

  /**
   * Remove all child nodes from an element.
   * @param {Element} el
   */
  wix.empty = function ( el ) {
    while ( el.firstChild ) {
      el.removeChild( el.firstChild );
    }
  };

  /**
   * Toggle a CSS class on an element.
   * @param {Element} el
   * @param {string} cls
   * @param {boolean} [force]
   */
  wix.toggle = function ( el, cls, force ) {
    if ( typeof force === 'boolean' ) {
      el.classList.toggle( cls, force );
    } else {
      el.classList.toggle( cls );
    }
  };


  /* ── 2. Event Delegation ──────────────────────────────────────── */

  /**
   * Attach a delegated event listener to a container.
   * Clicks (or other events) on descendants matching `selector`
   * trigger the callback with the matched element.
   *
   *   wix.on(container, 'click', '.wix-option', function (target, e) {
   *     // target is the .wix-option element
   *   });
   *
   * @param {Element} root
   * @param {string} eventType
   * @param {string} selector
   * @param {function(Element, Event)} callback
   */
  wix.on = function ( root, eventType, selector, callback ) {
    root.addEventListener( eventType, function ( e ) {
      var target = e.target.closest( selector );
      if ( target && root.contains( target ) ) {
        callback( target, e );
      }
    } );
  };


  /* ── 3. Data Attribute Helpers ────────────────────────────────── */

  /**
   * Read a data attribute, with an optional fallback.
   * @param {Element} el
   * @param {string} key   - Attribute name without "data-" prefix.
   * @param {*} [fallback]
   * @returns {string|*}
   */
  wix.data = function ( el, key, fallback ) {
    var val = el.getAttribute( 'data-' + key );
    return val !== null ? val : ( fallback !== undefined ? fallback : null );
  };

  /**
   * Write a data attribute.
   * @param {Element} el
   * @param {string} key
   * @param {string|number} value
   */
  wix.setData = function ( el, key, value ) {
    el.setAttribute( 'data-' + key, String( value ) );
  };


  /* ── 4. Module Init Guard ─────────────────────────────────────── */

  /**
   * Find all containers for a given module type that have not yet
   * been initialized, mark them, and return the list.
   *
   *   var quizzes = wix.initModules('quiz');
   *   quizzes.forEach(function (el) { ... });
   *
   * @param {string} moduleType  - Value of data-wix-module.
   * @returns {Element[]}
   */
  wix.initModules = function ( moduleType ) {
    var selector = '[data-wix-module="' + moduleType + '"]';
    var all = wix.qsa( selector );
    var fresh = [];
    all.forEach( function ( el ) {
      if ( !el.getAttribute( 'data-wix-init' ) ) {
        el.setAttribute( 'data-wix-init', '1' );
        fresh.push( el );
      }
    } );
    return fresh;
  };


  /* ── 5. Simple State Manager ──────────────────────────────────── */

  /**
   * Create a minimal reactive state object for a single widget instance.
   * State changes trigger a render callback.
   *
   *   var state = wix.createState({ step: 0, score: 0 }, render);
   *   state.set({ step: 1 });      // triggers render(newState)
   *   state.get().step;             // 1
   *
   * @param {Object} initial
   * @param {function(Object)} onChange
   * @returns {{ get: function():Object, set: function(Object) }}
   */
  wix.createState = function ( initial, onChange ) {
    var state = {};
    Object.keys( initial ).forEach( function ( k ) {
      state[ k ] = initial[ k ];
    } );

    return {
      get: function () {
        return state;
      },
      set: function ( patch ) {
        Object.keys( patch ).forEach( function ( k ) {
          state[ k ] = patch[ k ];
        } );
        if ( typeof onChange === 'function' ) {
          onChange( state );
        }
      }
    };
  };


  /* ── 6. UI Builders ───────────────────────────────────────────── */

  /**
   * Build a progress bar and return an object with an update method.
   *
   *   var bar = wix.buildProgressBar(container);
   *   bar.update(3, 10);  // "Question 3 of 10", fill 30%
   *
   * @param {Element} parent - Element to append the bar into.
   * @returns {{ el: Element, update: function(number, number) }}
   */
  wix.buildProgressBar = function ( parent ) {
    var label = wix.el( 'span', { className: 'wix-progress-label' } );
    var fill = wix.el( 'div', { className: 'wix-progress-fill' } );
    var track = wix.el( 'div', { className: 'wix-progress-track' }, [ fill ] );
    var wrap = wix.el( 'div', { className: 'wix-progress-wrap' }, [ label, track ] );

    parent.appendChild( wrap );

    return {
      el: wrap,
      update: function ( current, total ) {
        label.textContent = 'Question ' + current + ' of ' + total;
        fill.style.width = ( ( current / total ) * 100 ).toFixed( 1 ) + '%';
      }
    };
  };

  /**
   * Build a score ring (SVG circle) and return an object with a set method.
   *
   *   var ring = wix.buildScoreRing(container);
   *   ring.set(7, 10);  // 70 %, animates the ring fill
   *
   * @param {Element} parent
   * @returns {{ el: Element, set: function(number, number) }}
   */
  wix.buildScoreRing = function ( parent ) {
    var CIRCUMFERENCE = 264;
    var ns = 'http://www.w3.org/2000/svg';

    var bgCircle = document.createElementNS( ns, 'circle' );
    bgCircle.setAttribute( 'cx', '48' );
    bgCircle.setAttribute( 'cy', '48' );
    bgCircle.setAttribute( 'r', '42' );
    bgCircle.setAttribute( 'class', 'wix-ring-bg' );

    var fillCircle = document.createElementNS( ns, 'circle' );
    fillCircle.setAttribute( 'cx', '48' );
    fillCircle.setAttribute( 'cy', '48' );
    fillCircle.setAttribute( 'r', '42' );
    fillCircle.setAttribute( 'class', 'wix-ring-fill' );

    var svg = document.createElementNS( ns, 'svg' );
    svg.setAttribute( 'viewBox', '0 0 96 96' );
    svg.appendChild( bgCircle );
    svg.appendChild( fillCircle );

    var pct = wix.el( 'span', { className: 'wix-score-pct' } );
    var sub = wix.el( 'span', { className: 'wix-score-sub' } );
    var label = wix.el( 'div', { className: 'wix-score-label' }, [ pct, sub ] );

    var wrap = wix.el( 'div', { className: 'wix-score-ring' }, [ svg, label ] );
    parent.appendChild( wrap );

    return {
      el: wrap,
      set: function ( correct, total ) {
        var ratio = total > 0 ? correct / total : 0;
        var offset = CIRCUMFERENCE - ( ratio * CIRCUMFERENCE );
        pct.textContent = Math.round( ratio * 100 ) + '%';
        sub.textContent = correct + ' / ' + total + ' correct';
        // Delay to let the browser paint the initial state first
        requestAnimationFrame( function () {
          fillCircle.style.strokeDashoffset = offset;
        } );
      }
    };
  };

  /**
   * Build a feedback panel (correct / wrong) and return control methods.
   *
   *   var fb = wix.buildFeedback(card);
   *   fb.show('correct', 'Well done!', 'Because...');
   *   fb.hide();
   *
   * @param {Element} parent
   * @returns {{ el: Element, show: function(string, string, string), hide: function() }}
   */
  wix.buildFeedback = function ( parent ) {
    var strong = wix.el( 'strong' );
    var explanation = wix.el( 'span', { className: 'wix-explanation' } );
    var panel = wix.el( 'div', { className: 'wix-feedback' }, [ strong, explanation ] );

    parent.appendChild( panel );

    return {
      el: panel,
      show: function ( type, heading, detail ) {
        panel.className = 'wix-feedback wix-feedback--' + type + ' wix-feedback--show';
        strong.textContent = heading;
        explanation.textContent = detail || '';
      },
      hide: function () {
        panel.className = 'wix-feedback';
      }
    };
  };

  /**
   * Build a navigation bar with named buttons.
   *
   *   var nav = wix.buildNav(container, {
   *     next:    { label: 'Next', primary: true },
   *     restart: { label: 'Restart', outline: true }
   *   });
   *   nav.disable('next');
   *   nav.enable('next');
   *   nav.on('next', function () { ... });
   *   nav.show('restart');
   *   nav.hide('restart');
   *
   * @param {Element} parent
   * @param {Object} buttons  - { key: { label, primary?, outline?, hidden? } }
   * @returns {Object}
   */
  wix.buildNav = function ( parent, buttons ) {
    var bar = wix.el( 'div', { className: 'wix-nav' } );
    var map = {};
    var handlers = {};

    Object.keys( buttons ).forEach( function ( key ) {
      var cfg = buttons[ key ];
      var cls = 'wix-btn';
      if ( cfg.outline ) {
        cls += ' wix-btn--outline';
      }
      if ( cfg.hidden ) {
        cls += ' wix-hidden';
      }
      var btn = wix.el( 'button', { className: cls, textContent: cfg.label } );
      btn.addEventListener( 'click', function () {
        if ( handlers[ key ] ) {
          handlers[ key ]();
        }
      } );
      map[ key ] = btn;
      bar.appendChild( btn );
    } );

    parent.appendChild( bar );

    return {
      el: bar,
      disable: function ( key ) {
        if ( map[ key ] ) {
          map[ key ].disabled = true;
        }
      },
      enable: function ( key ) {
        if ( map[ key ] ) {
          map[ key ].disabled = false;
        }
      },
      show: function ( key ) {
        if ( map[ key ] ) {
          map[ key ].classList.remove( 'wix-hidden' );
        }
      },
      hide: function ( key ) {
        if ( map[ key ] ) {
          map[ key ].classList.add( 'wix-hidden' );
        }
      },
      on: function ( key, fn ) {
        handlers[ key ] = fn;
      },
      btn: function ( key ) {
        return map[ key ] || null;
      }
    };
  };


  /* ── 7. Utility Functions ─────────────────────────────────────── */

  /**
   * Shuffle an array in place (Fisher-Yates).
   * @param {Array} arr
   * @returns {Array} The same array, shuffled.
   */
  wix.shuffle = function ( arr ) {
    for ( var i = arr.length - 1; i > 0; i-- ) {
      var j = Math.floor( Math.random() * ( i + 1 ) );
      var tmp = arr[ i ];
      arr[ i ] = arr[ j ];
      arr[ j ] = tmp;
    }
    return arr;
  };

  /**
   * Clamp a number between min and max.
   * @param {number} val
   * @param {number} min
   * @param {number} max
   * @returns {number}
   */
  wix.clamp = function ( val, min, max ) {
    return Math.max( min, Math.min( max, val ) );
  };

  /**
   * Format a number with locale-appropriate thousand separators.
   * @param {number} n
   * @returns {string}
   */
  wix.formatNumber = function ( n ) {
    if ( typeof n !== 'number' ) {
      return String( n );
    }
    return n.toLocaleString();
  };


  /* ── 8. Expose Namespace ──────────────────────────────────────── */

  window.wix = wix;

}() );