MediaWiki:Gadget-wix-core.js
Appearance
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;
}() );