Jump to content

MediaWiki:Ticker2.js

From Wikinews, the free news source you can write!

Note: After saving, you may have to bypass your browser's cache to see the changes. Mozilla / Firefox / Safari: hold down Shift while clicking Reload, or press Ctrl-Shift-R (Cmd-Shift-R on Apple Mac); IE: hold Ctrl while clicking Refresh, or press Ctrl-F5; Konqueror: simply click the Reload button, or press F5; Opera users may need to completely clear their cache in Tools→Preferences. — More skins

/*globals Bawolff hookEvent */
/*
__This is Ticker2-0.9__

This is an attempt to redesign the ticker system. I believe that well the current ticker system is a good idea, it can be improved on

Then again, this may just be overthinking things. time will tell.
######
Design goals
*modular - should be able to have more then one transition between items
*user configurable - should be able to customize on wiki (aka {{ticker|for=Africa|speed=...}})
**The user should never have to do anything in js
**Downside: have to be extra-careful about XSS
*fast - performance was an issue with the other system

cavets:
*Doesn't handle multilevel lists well
*onblur/onfocus?? (works in opera, firefox, broken in MSIE)

Todo:
*change intro string to a control string with escape sequences (think printf)
**Play/pause button
*better fade transitions (current one kind of sucks)
*Change state system to number instead of T,F,U. 
*make fade work in IE
*multiple speed variables

####

Note: Setting disable_ticker2 to true will disable the ticker.
*/

//to avoid name conflicts (we already have way to many global variables as is imho)
//Everything should be a member of Bawolff.Ticker
if (typeof Bawolff !== "object") Bawolff = {};
if (typeof Bawolff.Ticker === "Object") throw new Error("Can not initilize ticker. Already initilized, or someone stole its name!");

Bawolff.Ticker = function() {
/*
This is the constructor function for new Ticker objects. Call as:
var some_ticker_object = new Bawolff.Ticker;

Its primary purpose is to set defaults and create new Ticker objects. It takes no arguments.
Ticker Objects should have the following methods:
*setUp - set up ticker properties (and does sanity checks)
*engines - functions for transitions
probably more
*/

    var t_s = Bawolff.Ticker; //shortcut

    this.arg = ""; //defaults to none, if uspecified. (should this be an arg to this function?). unphrased definition of options
    this.elm = null; //element to work ticker magic on. setUp method will throw an error if this isn't set by then 

    //method setUp will take care of these. These are the actual options (defaults here)

    this.engineNumb = t_s.eng.none; //constant representing engine number
    this.speed = 1; //float - multiplication factor
    //Probably can't set these to null through wiki
    this.strLeft = "Latest News ("; //Intro string part 1 - null for none
    this.strRight = "):"; //intro part 3 - null for none
    this.strLinkURI = "//en.wikinews.org/wiki/template:Latest_News"; //part 2 url
    this.strLinkText = "full list"; //part 2 text. null for no link.
    this.schowControls = false; //pause/restart
    this.tickSpeed = 1;
    this.resetSpeed=1;

    //Internal thingies (don't change)
    this.listIndex = 0;
    this.charIndex = 0;
    this.curState = "ok"; //for pausing
    this.resumeFunc = null; //storage for resume function
    this.resumeDelay = 0; //wait time before executing

};

Bawolff.Ticker.prototype.setUp = function() {
/*
This function takes no arguments. call as:
some_ticker_object.setUp();

It initilizes ticker options based on what the class name of the ticker element is.
The options are (mostly) encoded as follows in a class attribute:

Ticker_<option name- no dashes, underscore allowed>-<urlencoded option value(must end in alphanumeric or _ character)>

options are separated by a space (each is a different class).
###################################################
##This function must be extra careful not to be  ##
##vulnurable to an xss attack as it directly     ##
##deals with editable on wiki data, that could be##
##malicious! be careful                          ##
###################################################

*/


    //check to see if we really have an element
    if (!(this.elm && this.elm.nodeType && this.elm.nodeType === 1)) throw new Error("no element, or invalid element for ticker");

    var propMatch = /\bTicker_(\w+)-(\S*)\b/g;
    var res;
    var plusSign = /\+/g;
    
    while(res = propMatch.exec(this.arg)) {
       switch(res[1]) { //option name
            case "speed":
              var speed = parseFloat(decodeURIComponent(res[2]));
              if (isNaN(speed)) break;
              speed = 1/speed; //turn delay into speed multiplier
              if (speed < 1e-5) break;
              if (speed > 1e2)  break;
              
              this.speed = speed;
              break;

            case "strRight":
              res[2] = res[2].replace(plusSign, '%20');//encode + with % encode. does this work w/unicode
              res[2] = res[2].replace('%00', '');
              this.strRight = decodeURIComponent(res[2]);
              break;

            case "strLeft":
              res[2] = res[2].replace(plusSign, '%20');//encode + with % encode. does this work w/unicode
              res[2] = res[2].replace('%00', '');
              this.strLeft = decodeURIComponent(res[2]);
              break;

            case "strLinkText":
              res[2] = res[2].replace(plusSign, '%20');//encode + with % encode. does this work w/unicode
              res[2] = res[2].replace('%00', '');
              this.strLinkText = decodeURIComponent(res[2]);
              break;

            case "strLinkURI":
              //despite name, actually local page name, not a URI
              var page = encodeURIComponent(decodeURIComponent(res[2])); //note encode not decode
              if (page.match(/^special(%3A|:)userlogout/i)) break; //link is malicious
              this.strLinkURI = (page.length > 0) ? mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace("$1", page) : null;
              break;

            case "engine":
              //takes a literal engine name
              var engName = decodeURIComponent(res[2]);
              if (engName.match(/^\d+$/)||engName.length === 0) break;
              this.engineNumb = (typeof Bawolff.Ticker.eng[engName] === "number" ? Bawolff.Ticker.eng[engName] : this.engineNumb);
                
            default: 
              //throw new Error("not implemented");
        }
    }
    this.bigList = this.elm.getElementsByTagName("li"); //items to cycle ticker through
    this.listLength = this.bigList.length;
};
Bawolff.Ticker.prototype.start = function() {
//Start the ticker sets it up as well) separate from restart

/*
Creates a <ul class="actualTicker"> - actualTicker
            <span class="tickerIntroduction">(into <a class="tickerLink">stuff</a>):</span> - realTicker 
            <li > ...</li> (dummyItem)
          </ul>
*/

    this.elm.style.display = "none"; //hide the list

    var actualTicker = this.tickerElm = document.createElement("ul");
    actualTicker.className = "actualTicker";    

    var realTicker = document.createElement("Span");
    realTicker.className = "tickerIntroduction";
    realTicker.appendChild(document.createTextNode(this.strLeft));

    var realTickerLink = document.createElement("a");
    realTickerLink.href= this.strLinkURI;
    realTickerLink.title = this.strLinkText;
    realTickerLink.className = "tickerLink";
    realTickerLink.appendChild(document.createTextNode(this.strLinkText));
    realTicker.appendChild(realTickerLink);

    realTicker.appendChild(document.createTextNode(this.strRight));
    //Start the 2nd (Actual) span

    actualTicker.appendChild(realTicker);

    var dummyItem = document.createElement("li");
    actualTicker.appendChild(dummyItem);

    this.elm.parentNode.insertBefore(actualTicker, this.elm);

    this[Bawolff.Ticker.eng[this.engineNumb]](true); //Start the engine (ticker)



};
Bawolff.Ticker.prototype.pause = function () {
    this.curState = "paused";
};
Bawolff.Ticker.prototype.restart = function () {
    if (this.curState !== "paused") return false;
    this.curState = "ok";
    window.setTimeout(this.resumeFunc, this.resumeDelay);
};



/*
####
functions that are direct properties of the ticker constructor (not in prototype chain)
####
*/
Bawolff.Ticker.eng = []; //Stores object mapping engine name to engine number
Bawolff.Ticker.registerEngine = function (engName, engine) {
/*
This function takes care of hooking up engines (transitions)
into the system.
Arguments: String engName - name of engine (can not be a number)
           function engine - function containing engine code

Structure of what an engine should look like is noted somewhere (FIXME)

*/
    //to prevent screwing around with length property. considered an array index, even if passed a string with an interger value
    if (typeof engName !== "string" || (engName.match(/^\d+$/) !== null)) throw new Error("Invalid engine name. (can't be a number)");

    var te_s = Bawolff.Ticker.eng;
    var listLen = te_s.length;
    te_s[listLen] = "eng-" + engName;
    te_s[engName] = listLen;

    Bawolff.Ticker.prototype["eng-" + engName] = engine; //is that really the best way to add the engine functions?
};

Bawolff.Ticker.registerEngine("none", function(state) {
/*
This function is a dummy transition (no animation, but changes it)

called as:
tick() - advance one letter forward (unused)
tick(flase) - last tick before break
tick(true) - set up/first tick


*/
    if (!state) return true; //Shouldn't happen as null transition. normally can't do this

    if (state) {
        var newItem = this.bigList[this.listIndex].cloneNode(true); //true means deep
        this.tickerElm.replaceChild(newItem, this.tickerElm.lastChild);
        this.listIndex++;
        this.listIndex >= this.listLength ? this.listIndex = 0: true;

        var cur_obj = this; //needed, as otherwise executes in context of window
        var resF = function() {
            cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj, true);
        };
        var resD = 7000*cur_obj.speed*cur_obj.resetSpeed;
        if (this.curState === "paused") {
            this.resumeFunc = resF;
            this.resumeDelay = resD;
        } else { //assume "ok" but allow other states
            window.setTimeout(resF, resD);
        }

    }


});

Bawolff.Ticker.registerEngine("std", function(state) {
/*
This function is a standard - 1 char at a time ticker

//this is not the greatest done tick function. In a paticular it expects a list formated
// a specific way, and does not handle exceptional conditions as it should
//this should be fixed later

called as:
tick() - advance one letter forward (unused)
tick(flase) - last tick before break
tick(true) - set up/first tick


*/
    if (state === false) {
        this.tickerElm.lastChild.firstChild.firstChild.data = this.fullItem.substring(0,this.charIndex); // kill ...
        var cur_obj = this; //needed, as otherwise executes in context of window
        var resF = function() {
            cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj, true);
        };
        var resD = 7000*cur_obj.speed*cur_obj.resetSpeed;
        if (this.curState === "paused") {
            this.resumeFunc = resF;
            this.resumeDelay = resD;
        } else { //assume "ok" but allow other states
            window.setTimeout(resF, resD);
        }
    }

    if (state === void 0) { //undefined as in normal tick
        if (this.charIndex === this.fullItem.length) {
            //if we're done

            var cur_obj = this; //needed, as otherwise executes in context of window
            var resF = function() {
                cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj, false);
            };
            var resD = 60*cur_obj.speed*cur_obj.tickSpeed;
            if (this.curState === "paused") {
                this.resumeFunc = resF;
                this.resumeDelay = resD;
            } else { //assume "ok" but allow other states
                window.setTimeout(resF, resD);
            }
            return true;
        }

        this.charIndex++;
        this.tickerElm.lastChild.firstChild.firstChild.data = this.fullItem.substring(0,this.charIndex) + '...';

        var cur_obj = this; //needed, as otherwise executes in context of window
        var resF = function() {
            cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj);
        };
        var resD = 60*cur_obj.speed*cur_obj.tickSpeed;
        if (this.curState === "paused") {
            this.resumeFunc = resF;
            this.resumeDelay = resD;
        } else { //assume "ok" but allow other states
            window.setTimeout(resF, resD);
        }
        return true;

    }

    if (state) {
        var newItem = this.bigList[this.listIndex].cloneNode(true); //true means deep
//This still doesn't handle exceptional situations as good as possible, but it won't indef loop or freeze
        if (newItem.firstChild.firstChild !== null) { //Link and then text
            this.fullItem = newItem.firstChild.firstChild.data;    
            newItem.firstChild.firstChild.data = "";
        } else if (newItem.firstChild !== null) { //just text
            this.fullItem = newItem.firstChild.data;    
            newItem.replaceChild(document.createElement("span"), newItem.firstChild);
            newItem.firstChild.appendChild(document.createTextNode(""));
        } else { //input confused script. send error message
            newitem.insertBefore(document.createElement("strong"), null);
            newitem.firstChild.className = "error";
            newitem.firstChild.appendChild(document.createTextNode("Error: List item incorrectly formated for this ticker type. Please use unformatted text, or a single unformatted link (or otherwise one element deep)."));
            this.fullItem = newItem.firstChild.firstChild.data;
            newItem.firstChild.firstChild.data = "";
        }    
            
        this.charIndex = 0;

        this.tickerElm.replaceChild(newItem, this.tickerElm.lastChild);
        this.listIndex++;
        this.listIndex >= this.listLength ? this.listIndex = 0: true;

        var cur_obj = this; //needed, as otherwise executes in context of window
        cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj);

    }


});


Bawolff.Ticker.registerEngine("fade", function(state) {
/*
This function is a fade in effect

This is relies on Css3+MSIE extentions, and thus isn't all that cross browser compatible

called as:
tick() - advance one letter forward (unused)
tick(flase) - last tick before break
tick(true) - set up/first tick


*/

 
    if (state === false) { //sleep
        var cur_obj = this; //needed, as otherwise executes in context of window
        var resF = function() {
            cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj, true);
        };
        var resD = 7000*cur_obj.speed*cur_obj.resetSpeed;
        if (this.curState === "paused") {
            this.resumeFunc = resF;
            this.resumeDelay = resD;
        } else { //assume "ok" but allow other states
            window.setTimeout(resF, resD);
        }
    }

    if (state === void 0) { //undefined as in normal tick
        if (this.charIndex === 100) {
            //if we're done

            var cur_obj = this; //needed, as otherwise executes in context of window
            var resF = function() {
                cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj, false);
            };
            var resD = 40*cur_obj.speed*cur_obj.tickSpeed;
            if (this.curState === "paused") {
                this.resumeFunc = resF;
                this.resumeDelay = resD;
            } else { //assume "ok" but allow other states
                window.setTimeout(resF, resD);
            }
            return true;
        }

        this.charIndex++;
        Bawolff.setTrans(this.tickerElm.lastChild, this.charIndex/100);

        var cur_obj = this; //needed, as otherwise executes in context of window
        var resF = function() {
            cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj);
        }
        var resD = 40*cur_obj.speed*cur_obj.tickSpeed;
        if (this.curState === "paused") {
            this.resumeFunc = resF;
            this.resumeDelay = resD;
        } else { //assume "ok" but allow other states
            window.setTimeout(resF, resD);
        }
        return true;

    }

        if (state) {

        this.charIndex = 0;

        var newItem = this.bigList[this.listIndex].cloneNode(true); //true means deep
        Bawolff.setTrans(newItem, 0);
        (navigator && navigator.appName === "Microsoft Internet Explorer") ? newItem.style.display = 'inline-block' : true;
        this.tickerElm.replaceChild(newItem, this.tickerElm.lastChild);
        this.listIndex++;
        this.listIndex >= this.listLength ? this.listIndex = 0: true;


        var cur_obj = this; //needed, as otherwise executes in context of window
        cur_obj[Bawolff.Ticker.eng[cur_obj.engineNumb]].call(cur_obj);

    }

});



/*Not really used. To make all pause call Bawolff.Ticker.allDo("pause");
Bawolff.Ticker.allDo =  function (func) {
    var l = Bawolff.Ticker.allTickers.length;
    for (var i=0;i<l;i++) {
        Bawolff.Ticker.allTickers[i][func]();
    }
}
*/
Bawolff.Ticker.allDoPause =  function () {
    var l = Bawolff.Ticker.allTickers.length;
    for (var i=0;i<l;i++) {
        Bawolff.Ticker.allTickers[i].pause();
    }
}
Bawolff.Ticker.allDoRestart =  function () {
    var l = Bawolff.Ticker.allTickers.length;
    for (var i=0;i<l;i++) {
        Bawolff.Ticker.allTickers[i].restart();
    }
}

Bawolff.setTrans = function(elm, opacity/*1 being full visible, 0 being invisible*/) {
    if (!Bawolff.setTrans.opacityMethod) {
        //standard way (CSS3)
        if (elm.style && (typeof elm.style.opacity != "undefined")) {
            Bawolff.setTrans.opacityMethod = 1;
        }
        else if (elm.style && (typeof elm.style.MozOpacity != "undefined")) { //old moz
            Bawolff.setTrans.opacityMethod = 2;
        }
        else if (elm.style && (typeof elm.style.filter != "undefined")) {
            Bawolff.setTrans.opacityMethod = 3;
        }
        else {
            
            //throw new Error("opacity is not supported on this platform (or this script needs to be fixed to include support on your platform");
        }
    }
    switch (Bawolff.setTrans.opacityMethod) {
        case 1:
            elm.style.opacity = opacity;
            break;
        case 2:
            elm.style.MozOpacity = opacity;
            break;
        case 3:
            elm.style.filter = "alpha(opacity=" + opacity*100 + ")"; //No guarantees this works
            break;
        default:
            //do nothing, so other browsers not inconvianced
            break;
    }
}

Bawolff.Ticker.init = function () {
    //handled elsewhere if (!document.getElementById("enableTickers")) return false; //Bcause getting all elements by class is expensive

    var tickerList = document.getElementsByClassName("isATicker");
    var l = tickerList.length;
    var i = 0; //index of which ticker we are on.

    Bawolff.Ticker.allTickers = [];

    if (document.getElementById("singleTickerForPage")) { //for simplifications. if only one on page
        Bawolff.Ticker.allTickers[i] = new Bawolff.Ticker;
        Bawolff.Ticker.allTickers[i].elm = document.getElementById("singleTickerForPage");
        Bawolff.Ticker.allTickers[i].arg = document.getElementById("singleTickerForPage").className;

        Bawolff.Ticker.allTickers[i].setUp();
        Bawolff.Ticker.allTickers[i].start();
        i++;
    }

    for (;i<l;i++) {
        Bawolff.Ticker.allTickers[i] = new Bawolff.Ticker;
        Bawolff.Ticker.allTickers[i].elm = tickerList[i];
        Bawolff.Ticker.allTickers[i].arg = tickerList[i].className;

        Bawolff.Ticker.allTickers[i].setUp();
        Bawolff.Ticker.allTickers[i].start();
    }
    


    if (!(navigator && navigator.appName === "Microsoft Internet Explorer")) {
    //blur sometimes fires too much on MSIE and makes things not work
    hookEvent('blur', Bawolff.Ticker.allDoPause);//stop anim on loss of focus, and restart it on gain of focus. hookEvent from wikibits
    hookEvent('focus', Bawolff.Ticker.allDoRestart);
    }

}
Bawolff.Ticker.init(); //already from a load event