MediaWiki:Common.js: Difference between revisions

Highlight table: Wait until required MediaWiki modules are ready
(Amend sticky headers to avoid compatibility issues with tabber containers)
(Highlight table: Wait until required MediaWiki modules are ready)
Line 89: Line 89:
     onevar:true
     onevar:true
*/
*/
mw.loader.using(['oojs']).then(function() {
(function($, mw) {
    'use strict';
    var hasLocalStorage = function(){
        try {
            localStorage.setItem('test', 'test')
            localStorage.removeItem('test')
            return true
        } catch (e) {
            return false
        }
    }
    // constants
    var STORAGE_KEY = 'mi:lightTable',
        TABLE_CLASS = 'lighttable',
        LIGHT_ON_CLASS = 'highlight-on',
        MOUSE_OVER_CLASS = 'highlight-over',
        BASE_64_URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
        PAGE_SEPARATOR = '!',
        TABLE_SEPARATOR = '.',
        CASTAGNOLI_POLYNOMIAL = 0x04c11db7,
        UINT32_MAX = 0xffffffff,
        self = {
            /*
            * Stores the current uncompressed data for the current page.
            */
            data: null,
            /*
            * Perform initial checks on the page and browser.
            */
            init: function() {
                var $tables = $('table.' + TABLE_CLASS),
                    hashedPageName = self.hashString(mw.config.get('wgPageName'));
                // check we have some tables to interact with
                if (!$tables.length) {
                    return;
                }
                // check the browser supports local storage
                if (!hasLocalStorage()) {
                    return;
                }
                self.data = self.load(hashedPageName, $tables.length);
                self.initTables(hashedPageName, $tables);
            },
            /*
            * Initialise table highlighting.
            *
            * @param hashedPageName The current page name as a hash.
            * @param $tables A list of highlightable tables on the current page.
            */
            initTables: function(hashedPageName, $tables) {
                $tables.each(function(tIndex) {
                    var $this = $(this),
                        // data cells
                        $cells = $this.find('td'),
                        $rows = $this.find('tr:has(td)'),
                        // don't rely on headers to find number of columns     
                        // count them dynamically
                        columns = 1,
                        tableData = self.data[tIndex],
                        mode = 'cells';
                    // Switching between either highlighting rows or cells
                    if (!$this.hasClass('individual')) {
                        mode = 'rows';
                        $cells = $rows;
                    }
                    // initialise rows if necessary
                    while ($cells.length > tableData.length) {
                        tableData.push(0);
                    }
                    // counting the column count
                    // necessary to determine colspan of reset button
                    $rows.each(function() {
                        var $this = $(this);
                        columns = Math.max(columns, $this.children('th,td').length);
                    });
                    $cells.each(function(cIndex) {
                        var $this = $(this),
                            cellData = tableData[cIndex];
                        // forbid highlighting any cells/rows that have class nohighlight
                        if (!$this.hasClass('nohighlight')) {
                            // initialize highlighting based on the cookie
                            self.setHighlight($this, cellData);
                            // set mouse events
                            $this
                                .mouseover(function() {
                                    self.setHighlight($this, 2);
                                })
                                .mouseout(function() {
                                    self.setHighlight($this, tableData[cIndex]);
                                })
                                .click(function(e) {
                                    // don't toggle highlight when clicking links
                                    if ((e.target.tagName !== 'A') && (e.target.tagName !== 'IMG')) {
                                        // 1 -> 0
                                        // 0 -> 1
                                        tableData[cIndex] = 1 - tableData[cIndex];
                                        self.setHighlight($this, tableData[cIndex]);
                                        self.save(hashedPageName);
                                    }
                                });
                        }
                    });
                    // add a button for reset
                    var button = new OO.ui.ButtonWidget({
                        label: 'Clear selection',
                        icon: 'clear',
                        title: 'Removes all highlights from the table',
                        classes: ['ht-reset'] // this class is targeted by other gadgets, be careful removing it
                    });
                    button.$element.click(function() {
                        $cells.each(function(cIndex) {
                            tableData[cIndex] = 0;
                            self.setHighlight($(this), 0);
                        });
                        self.save(hashedPageName, $tables.length);
                    });
                    $this.append(
                        $('<tfoot>')
                            .append(
                                $('<tr>')
                                    .append(
                                        $('<th>')
                                            .attr('colspan', columns)
                                            .append(button.$element)
                                    )
                            )
                    );
                });
            },
            /*
            * Change the cell background color based on mouse events.
            *
            * @param $cell The cell element.
            * @param val The value to control what class to add (if any).
            *            0 -> light off (no class)
            *            1 -> light on
            *            2 -> mouse over
            */
            setHighlight: function($cell, val) {
                $cell.removeClass(MOUSE_OVER_CLASS);
                $cell.removeClass(LIGHT_ON_CLASS);
                switch (val) {
                    // light on
                    case 1:
                        $cell.addClass(LIGHT_ON_CLASS);
                        break;
                    // mouse-over
                    case 2:
                        $cell.addClass(MOUSE_OVER_CLASS);
                        break;
                }
            },
            /*
            * Merge the updated data for the current page into the data for other pages into local storage.
            *
            * @param hashedPageName A hash of the current page name.
            */
            save: function(hashedPageName) {
                // load the existing data so we know where to save it
                var curData = localStorage.getItem(STORAGE_KEY),
                    compressedData;
                if (curData === null) {
                    curData = {};
                } else {
                    curData = JSON.parse(curData);
                    curData = self.parse(curData);
                }
                // merge in our updated data and compress it
                curData[hashedPageName] = self.data;
                compressedData = self.compress(curData);
                // convert to a string and save to localStorage
                compressedData = JSON.stringify(compressedData);
                localStorage.setItem(STORAGE_KEY, compressedData);
            },
            /*
            * Compress the entire data set using tha algoritm documented at the top of the page.
            *
            * @param data The data to compress.
            *
            * @return the compressed data.
            */
            compress: function(data) {
                var ret = {};
                Object.keys(data).forEach(function(hashedPageName) {
                    var pageData = data[hashedPageName],
                        pageKey = hashedPageName.charAt(0);
                    if (!ret.hasOwnProperty(pageKey)) {
                        ret[pageKey] = {};
                    }
                    ret[pageKey][hashedPageName] = [];
                    pageData.forEach(function(tableData) {
                        var compressedTableData = '',
                            i, j, k;
                        for (i = 0; i < Math.ceil(tableData.length / 6); i += 1) {
                            k = tableData[6 * i];
                            for (j = 1; j < 6; j += 1) {
                                k = 2 * k + ((6 * i + j < tableData.length) ? tableData[6 * i + j] : 0);
                            }
                            compressedTableData += BASE_64_URL.charAt(k);
                        }
                        ret[pageKey][hashedPageName].push(compressedTableData);
                    });
                    ret[pageKey][hashedPageName] = ret[pageKey][hashedPageName].join(TABLE_SEPARATOR);
                });
                Object.keys(ret).forEach(function(pageKey) {
                    var hashKeys = Object.keys(ret[pageKey]),
                        hashedData = [];
                    hashKeys.forEach(function(key) {
                        var pageData = ret[pageKey][key];
                        hashedData.push(key + pageData);
                    });
                    hashedData = hashedData.join(PAGE_SEPARATOR);
                    ret[pageKey] = hashedData;
                });
                return ret;
            },
            /*
            * Get the existing data for the current page.
            *
            * @param hashedPageName A hash of the current page name.
            * @param numTables The number of tables on the current page. Used to ensure the loaded
            *                  data matches the number of tables on the page thus handling cases
            *                  where tables have been added or removed. This does not check the
            *                  amount of rows in the given tables.
            *
            * @return The data for the current page.
            */
            load: function(hashedPageName, numTables) {
                var data = localStorage.getItem(STORAGE_KEY),
                    pageData;
                if (data === null) {
                    pageData = [];
                } else {
                    data = JSON.parse(data);
                    data = self.parse(data);
                    if (data.hasOwnProperty(hashedPageName)) {
                        pageData = data[hashedPageName];
                    } else {
                        pageData = [];
                    }
                }
                // if more tables were added
                // add extra arrays to store the data in
                // also populates if no existing data was found
                while (numTables > pageData.length) {
                    pageData.push([]);
                }
                // if tables were removed, remove data from the end of the list
                // as there's no way to tell which was removed
                while (numTables < pageData.length) {
                    pageData.pop();
                }
                return pageData;
            },
            /*
            * Parse the compressed data as loaded from local storage using the algorithm desribed
            * at the top of the page.
            *
            * @param data The data to parse.
            *
            * @return the parsed data.
            */
            parse: function(data) {
                var ret = {};
                Object.keys(data).forEach(function(pageKey) {
                    var pageData = data[pageKey].split(PAGE_SEPARATOR);
                    pageData.forEach(function(tableData) {
                        var hashedPageName = tableData.substr(0, 8);
                        tableData = tableData.substr(8).split(TABLE_SEPARATOR);
                        ret[hashedPageName] = [];
                        tableData.forEach(function(rowData, index) {
                            var i, j, k;
                            ret[hashedPageName].push([]);
                            for (i = 0; i < rowData.length; i += 1) {
                                k = BASE_64_URL.indexOf(rowData.charAt(i));
                                // input validation
                                if (k < 0) {
                                    k = 0;
                                }
                                for (j = 5; j >= 0; j -= 1) {
                                    ret[hashedPageName][index][6 * i + j] = (k & 0x1);
                                    k >>= 1;
                                }
                            }
                        });
                    });
                });
                return ret;
            },
            /*
            * Hash a string into a big endian 32 bit hex string. Used to hash page names.
            *
            * @param input The string to hash.
            *
            * @return the result of the hash.
            */
            hashString: function(input) {
                var ret = 0,
                    table = [],
                    i, j, k;
                // guarantee 8-bit chars
                input = window.unescape(window.encodeURI(input));
                // calculate the crc (cyclic redundancy check) for all 8-bit data
                // bit-wise operations discard anything left of bit 31
                for (i = 0; i < 256; i += 1) {
                    k = (i << 24);
                    for (j = 0; j < 8; j += 1) {
                        k = (k << 1) ^ ((k >>> 31) * CASTAGNOLI_POLYNOMIAL);
                    }
                    table[i] = k;
                }
                // the actual calculation
                for (i = 0; i < input.length; i += 1) {
                    ret = (ret << 8) ^ table[(ret >>> 24) ^ input.charCodeAt(i)];
                }
                // make negative numbers unsigned
                if (ret < 0) {
                    ret += UINT32_MAX;
                }
                // 32-bit hex string, padded on the left
                ret = '0000000' + ret.toString(16).toUpperCase();
                ret = ret.substr(ret.length - 8);
                return ret;
            }
        };
    $(self.init);
    /*
    // sample data for testing the algorithm used
    var data = {
        // page1
        '0FF47C63': [
            [0, 1, 1, 0, 1, 0],
            [0, 1, 1, 0, 1, 0, 1, 1, 1],
            [0, 0, 0, 0, 1, 1, 0, 0]
        ],
        // page2
        '02B75ABA': [
            [0, 1, 0, 1, 1, 0],
            [1, 1, 1, 0, 1, 0, 1, 1, 0],
            [0, 0, 1, 1, 0, 0, 0, 0]
        ],
        // page3
        '0676470D': [
            [1, 0, 0, 1, 0, 1],
            [1, 0, 0, 1, 0, 1, 0, 0, 0],
            [1, 1, 1, 1, 0, 0, 1, 1]
        ]
    };
    console.log('input', data);
    var compressedData = self.compress(data);
    console.log('compressed', compressedData);
    var parsedData = self.parse(compressedData);
    console.log(parsedData);
    */


(function($, mw) {
}(this.jQuery, this.mediaWiki));
    'use strict';
});
 
    var hasLocalStorage = function(){
        try {
            localStorage.setItem('test', 'test')
            localStorage.removeItem('test')
            return true
        } catch (e) {
            return false
        }
    }
 
    // constants
    var STORAGE_KEY = 'mi:lightTable',
        TABLE_CLASS = 'lighttable',
        LIGHT_ON_CLASS = 'highlight-on',
        MOUSE_OVER_CLASS = 'highlight-over',
        BASE_64_URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
        PAGE_SEPARATOR = '!',
        TABLE_SEPARATOR = '.',
        CASTAGNOLI_POLYNOMIAL = 0x04c11db7,
        UINT32_MAX = 0xffffffff,
 
        self = {
            /*
            * Stores the current uncompressed data for the current page.
            */
            data: null,
 
            /*
            * Perform initial checks on the page and browser.
            */
            init: function() {
                var $tables = $('table.' + TABLE_CLASS),
                    hashedPageName = self.hashString(mw.config.get('wgPageName'));
 
                // check we have some tables to interact with
                if (!$tables.length) {
                    return;
                }
                // check the browser supports local storage
                if (!hasLocalStorage()) {
                    return;
                }
 
                self.data = self.load(hashedPageName, $tables.length);
                self.initTables(hashedPageName, $tables);
            },
 
            /*
            * Initialise table highlighting.
            *
            * @param hashedPageName The current page name as a hash.
            * @param $tables A list of highlightable tables on the current page.
            */
            initTables: function(hashedPageName, $tables) {
                $tables.each(function(tIndex) {
                    var $this = $(this),
                        // data cells
                        $cells = $this.find('td'),
                        $rows = $this.find('tr:has(td)'),
                        // don't rely on headers to find number of columns     
                        // count them dynamically
                        columns = 1,
                        tableData = self.data[tIndex],
                        mode = 'cells';
 
                    // Switching between either highlighting rows or cells
                    if (!$this.hasClass('individual')) {
                        mode = 'rows';
                        $cells = $rows;
                    }
 
                    // initialise rows if necessary
                    while ($cells.length > tableData.length) {
                        tableData.push(0);
                    }
 
                    // counting the column count
                    // necessary to determine colspan of reset button
                    $rows.each(function() {
                        var $this = $(this);
                        columns = Math.max(columns, $this.children('th,td').length);
                    });
 
                    $cells.each(function(cIndex) {
                        var $this = $(this),
                            cellData = tableData[cIndex];
 
                        // forbid highlighting any cells/rows that have class nohighlight
                        if (!$this.hasClass('nohighlight')) {
                            // initialize highlighting based on the cookie
                            self.setHighlight($this, cellData);
 
                            // set mouse events
                            $this
                                .mouseover(function() {
                                    self.setHighlight($this, 2);
                                })
                                .mouseout(function() {
                                    self.setHighlight($this, tableData[cIndex]);
                                })
                                .click(function(e) {
                                    // don't toggle highlight when clicking links
                                    if ((e.target.tagName !== 'A') && (e.target.tagName !== 'IMG')) {
                                        // 1 -> 0
                                        // 0 -> 1
                                        tableData[cIndex] = 1 - tableData[cIndex];
 
                                        self.setHighlight($this, tableData[cIndex]);
                                        self.save(hashedPageName);
                                    }
                                });
                        }
                    });
 
                    // add a button for reset
                    var button = new OO.ui.ButtonWidget({
                        label: 'Clear selection',
                        icon: 'clear',
                        title: 'Removes all highlights from the table',
                        classes: ['ht-reset'] // this class is targeted by other gadgets, be careful removing it
                    });
 
 
                    button.$element.click(function() {
                        $cells.each(function(cIndex) {
                            tableData[cIndex] = 0;
                            self.setHighlight($(this), 0);
                        });
 
                        self.save(hashedPageName, $tables.length);
                    });
 
                    $this.append(
                        $('<tfoot>')
                            .append(
                                $('<tr>')
                                    .append(
                                        $('<th>')
                                            .attr('colspan', columns)
                                            .append(button.$element)
                                    )
                            )
                    );
                });
            },
 
            /*
            * Change the cell background color based on mouse events.
            *
            * @param $cell The cell element.
            * @param val The value to control what class to add (if any).
            *            0 -> light off (no class)
            *            1 -> light on
            *            2 -> mouse over
            */
            setHighlight: function($cell, val) {
                $cell.removeClass(MOUSE_OVER_CLASS);
                $cell.removeClass(LIGHT_ON_CLASS);
 
                switch (val) {
                    // light on
                    case 1:
                        $cell.addClass(LIGHT_ON_CLASS);
                        break;
 
                    // mouse-over
                    case 2:
                        $cell.addClass(MOUSE_OVER_CLASS);
                        break;
                }
            },
 
            /*
            * Merge the updated data for the current page into the data for other pages into local storage.
            *
            * @param hashedPageName A hash of the current page name.
            */
            save: function(hashedPageName) {
                // load the existing data so we know where to save it
                var curData = localStorage.getItem(STORAGE_KEY),
                    compressedData;
 
                if (curData === null) {
                    curData = {};
                } else {
                    curData = JSON.parse(curData);
                    curData = self.parse(curData);
                }
 
                // merge in our updated data and compress it
                curData[hashedPageName] = self.data;
                compressedData = self.compress(curData);
 
                // convert to a string and save to localStorage
                compressedData = JSON.stringify(compressedData);
                localStorage.setItem(STORAGE_KEY, compressedData);
            },
 
            /*
            * Compress the entire data set using tha algoritm documented at the top of the page.
            *
            * @param data The data to compress.
            *
            * @return the compressed data.
            */
            compress: function(data) {
                var ret = {};
 
                Object.keys(data).forEach(function(hashedPageName) {
                    var pageData = data[hashedPageName],
                        pageKey = hashedPageName.charAt(0);
 
                    if (!ret.hasOwnProperty(pageKey)) {
                        ret[pageKey] = {};
                    }
 
                    ret[pageKey][hashedPageName] = [];
 
                    pageData.forEach(function(tableData) {
                        var compressedTableData = '',
                            i, j, k;
 
                        for (i = 0; i < Math.ceil(tableData.length / 6); i += 1) {
                            k = tableData[6 * i];
 
                            for (j = 1; j < 6; j += 1) {
                                k = 2 * k + ((6 * i + j < tableData.length) ? tableData[6 * i + j] : 0);
                            }
 
                            compressedTableData += BASE_64_URL.charAt(k);
                        }
 
                        ret[pageKey][hashedPageName].push(compressedTableData);
                    });
 
                    ret[pageKey][hashedPageName] = ret[pageKey][hashedPageName].join(TABLE_SEPARATOR);
                });
 
                Object.keys(ret).forEach(function(pageKey) {
                    var hashKeys = Object.keys(ret[pageKey]),
                        hashedData = [];
 
                    hashKeys.forEach(function(key) {
                        var pageData = ret[pageKey][key];
                        hashedData.push(key + pageData);
                    });
 
                    hashedData = hashedData.join(PAGE_SEPARATOR);
                    ret[pageKey] = hashedData;
                });
 
                return ret;
            },
 
            /*
            * Get the existing data for the current page.
            *
            * @param hashedPageName A hash of the current page name.
            * @param numTables The number of tables on the current page. Used to ensure the loaded
            *                  data matches the number of tables on the page thus handling cases
            *                  where tables have been added or removed. This does not check the
            *                  amount of rows in the given tables.
            *
            * @return The data for the current page.
            */
            load: function(hashedPageName, numTables) {
                var data = localStorage.getItem(STORAGE_KEY),
                    pageData;
 
                if (data === null) {
                    pageData = [];
                } else {
                    data = JSON.parse(data);
                    data = self.parse(data);
 
                    if (data.hasOwnProperty(hashedPageName)) {
                        pageData = data[hashedPageName];
                    } else {
                        pageData = [];
                    }
                }
 
                // if more tables were added
                // add extra arrays to store the data in
                // also populates if no existing data was found
                while (numTables > pageData.length) {
                    pageData.push([]);
                }
 
                // if tables were removed, remove data from the end of the list
                // as there's no way to tell which was removed
                while (numTables < pageData.length) {
                    pageData.pop();
                }
 
                return pageData;
            },
 
            /*
            * Parse the compressed data as loaded from local storage using the algorithm desribed
            * at the top of the page.
            *
            * @param data The data to parse.
            *
            * @return the parsed data.
            */
            parse: function(data) {
                var ret = {};
 
                Object.keys(data).forEach(function(pageKey) {
                    var pageData = data[pageKey].split(PAGE_SEPARATOR);
 
                    pageData.forEach(function(tableData) {
                        var hashedPageName = tableData.substr(0, 8);
 
                        tableData = tableData.substr(8).split(TABLE_SEPARATOR);
                        ret[hashedPageName] = [];
 
                        tableData.forEach(function(rowData, index) {
                            var i, j, k;
 
                            ret[hashedPageName].push([]);
 
                            for (i = 0; i < rowData.length; i += 1) {
                                k = BASE_64_URL.indexOf(rowData.charAt(i));
 
                                // input validation
                                if (k < 0) {
                                    k = 0;
                                }
 
                                for (j = 5; j >= 0; j -= 1) {
                                    ret[hashedPageName][index][6 * i + j] = (k & 0x1);
                                    k >>= 1;
                                }
                            }
                        });
                    });
 
                });
 
                return ret;
            },
 
            /*
            * Hash a string into a big endian 32 bit hex string. Used to hash page names.
            *
            * @param input The string to hash.
            *
            * @return the result of the hash.
            */
            hashString: function(input) {
                var ret = 0,
                    table = [],
                    i, j, k;
 
                // guarantee 8-bit chars
                input = window.unescape(window.encodeURI(input));
 
                // calculate the crc (cyclic redundancy check) for all 8-bit data
                // bit-wise operations discard anything left of bit 31
                for (i = 0; i < 256; i += 1) {
                    k = (i << 24);
 
                    for (j = 0; j < 8; j += 1) {
                        k = (k << 1) ^ ((k >>> 31) * CASTAGNOLI_POLYNOMIAL);
                    }
                    table[i] = k;
                }
 
                // the actual calculation
                for (i = 0; i < input.length; i += 1) {
                    ret = (ret << 8) ^ table[(ret >>> 24) ^ input.charCodeAt(i)];
                }
 
                // make negative numbers unsigned
                if (ret < 0) {
                    ret += UINT32_MAX;
                }
 
                // 32-bit hex string, padded on the left
                ret = '0000000' + ret.toString(16).toUpperCase();
                ret = ret.substr(ret.length - 8);
 
                return ret;
            }
        };
 
    $(self.init);
 
    /*
    // sample data for testing the algorithm used
    var data = {
        // page1
        '0FF47C63': [
            [0, 1, 1, 0, 1, 0],
            [0, 1, 1, 0, 1, 0, 1, 1, 1],
            [0, 0, 0, 0, 1, 1, 0, 0]
        ],
        // page2
        '02B75ABA': [
            [0, 1, 0, 1, 1, 0],
            [1, 1, 1, 0, 1, 0, 1, 1, 0],
            [0, 0, 1, 1, 0, 0, 0, 0]
        ],
        // page3
        '0676470D': [
            [1, 0, 0, 1, 0, 1],
            [1, 0, 0, 1, 0, 1, 0, 0, 0],
            [1, 1, 1, 1, 0, 0, 1, 1]
        ]
    };
 
    console.log('input', data);
 
    var compressedData = self.compress(data);
    console.log('compressed', compressedData);
 
    var parsedData = self.parse(compressedData);
    console.log(parsedData);
    */
 
}(this.jQuery, this.mediaWiki));


// </pre>
// </pre>