|
|
| Line 1: |
Line 1: |
| − | /** <pre>
| + | var numWords = $("#mw-content-text > div").text().split(" ").length; |
| − | * highlightTable.js
| + | var headerWords = $("h1").text().split(" ").length; |
| − | *
| + | var totalWords = numWords + headerWords; |
| − | * Description:
| + | var timeInMinutes = totalWords / 200; |
| − | * Adds highlighting to tables
| + | var header = $("h1").text(); |
| − | *
| + | $("h1").text(header + " (it will take you " + timeInMinutes + " minutes to read this page)"); |
| − | * History:
| |
| − | * - 1.0: Row highlighting - Quarenon
| |
| − | * - 1.1: Update from pengLocations.js v1.0 - Quarenon
| |
| − | * - 2.0: pengLocations v2.1, Granular cookie - Saftzie
| |
| − | * - 2.1: Made compatible with jquery.tablesorter - Cqm
| |
| − | * - 2.2: Switch to localStorage - Cqm
| |
| − | * - 3.0: Allow cell highlighting - mejrs
| |
| − | *
| |
| − | * @todo Allow the stored data to be coupled to the table in question. Currently the data is stored
| |
| − | * on the page itself, so if any tables are shuffled, the highlighting doesn't follow. For
| |
| − | * the same reason tables hosted on other pages are not synchronized.
| |
| − | */
| |
| − | | |
| − | /**
| |
| − | * DATA STORAGE STRUCTURE
| |
| − | * ----------------------
| |
| − | *
| |
| − | * In its raw, uncompressed format, the stored data is as follows:
| |
| − | * {
| |
| − | * hashedPageName1: [
| |
| − | * [0, 1, 0, 1, 0, 1],
| |
| − | * [1, 0, 1, 0, 1, 0],
| |
| − | * [0, 0, 0, 0, 0, 0]
| |
| − | * ],
| |
| − | * hashedPageName2: [
| |
| − | * [0, 1, 0, 1, 0, 1],
| |
| − | * [1, 0, 1, 0, 1, 0],
| |
| − | * [0, 0, 0, 0, 0, 0]
| |
| − | * ]
| |
| − | * }
| |
| − | *
| |
| − | * Where `hashedPageNameX` is the value of wgPageName passed through our `hashString` function,
| |
| − | * the arrays of numbers representing tables on a page (from top to bottom) and the numbers
| |
| − | * representing whether a row is highlighted or not, depending on if it is 1 or 0 respectively.
| |
| − | *
| |
| − | * During compression, these numbers are collected into groups of 6 and converted to base64.
| |
| − | * For example:
| |
| − | *
| |
| − | * 1. [0, 1, 0, 1, 0, 1]
| |
| − | * 2. 0x010101 (1 + 4 + 16 = 21)
| |
| − | * 3. BASE_64_URL[21] (U)
| |
| − | *
| |
| − | * Once each table's rows have been compressed into strings, they are concatenated using `.` as a
| |
| − | * delimiter. The hashed page name (which is guaranteed to be 8 characters long) is then prepended
| |
| − | * to this string to look something like the following:
| |
| − | *
| |
| − | * XXXXXXXXab.dc.ef
| |
| − | *
| |
| − | *
| |
| − | * The first character of a hashed page name is then used to form the object that is actually
| |
| − | * stored. As the hashing function uses hexadecimal, this gives us 16 possible characters (0-9A-Z).
| |
| − | *
| |
| − | * {
| |
| − | * A: ...
| |
| − | * B: ...
| |
| − | * C: ...
| |
| − | * // etc.
| |
| − | * }
| |
| − | *
| |
| − | * The final step of compression is to merge each page's data together under it's respective top
| |
| − | * level key. this is done by concatenation again, separated by a `!`.
| |
| − | *
| |
| − | * The resulting object is then converted to a string and persisted in local storage. When
| |
| − | * uncompressing data, simply perform the following steps in reverse.
| |
| − | *
| |
| − | * For the implementation of this algorithm, see:
| |
| − | * - `compress`
| |
| − | * - `parse`
| |
| − | * - `hashString`
| |
| − | *
| |
| − | * Note that while rows could theoretically be compressed further by using all ASCII characters,
| |
| − | * eventually we'd start using characters outside printable ASCII which makes debugging painful.
| |
| − | */
| |
| − | | |
| − | /*jshint bitwise:false, camelcase:true, curly:true, eqeqeq:true, es3:false,
| |
| − | forin:true, immed:true, indent:4, latedef:true, newcap:true,
| |
| − | noarg:true, noempty:true, nonew:true, plusplus:true, quotmark:single,
| |
| − | undef:true, unused:true, strict:true, trailing:true,
| |
| − | browser:true, devel:false, jquery:true,
| |
| − | onevar:true
| |
| − | */
| |
| − | | |
| − | (function($, mw, OO, rs) {
| |
| − | 'use strict';
| |
| − | | |
| − | // constants
| |
| − | var STORAGE_KEY = 'rs: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 (!rs.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: (mode === 'rows') ?
| |
| − | 'Clear highlighted rows' :
| |
| − | 'Clear highlighted cells',
| |
| − | 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, this.OO, this.rswiki));
| |
| − | | |
| − | // </pre>
| |