Note: After saving, changes may not occur immediately. Click here to learn how to bypass your browser's cache.
  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Cmd-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (Cmd-Shift-R on a Mac)
  • Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Clear the cache in Tools → Preferences

For details and instructions about other browsers, see Wikipedia:Bypass your cache.

//
// Tool to helpfully preview page in an index (good for checking page numbers)
//

(function($, mw) {
  "use strict";

  function install_css(css) {
    $('head').append(`<style type="text/css">${css}</style>`);
  }

  var QUALITY = {
    WITHOUT_TEXT: 0,
    NOT_PROOFREAD: 1,
    PROBLEMATIC: 2,
    PROOFREAD: 3,
    VALIDATED: 4,
  };

  var Preview = {
    strings: {
      show_grid: 'Show page grid',
      show_grid_tooltip: 'Show a grid of all page images',
      hide_grid: 'Hide page grid',
      hide_grid_tooltip: 'Hide the grid of all page images',
      mark_without_text: 'Without text',
      without_text_summary: '/* Without text */',
      problematic_summary: "/* Problematic */",
      created_ok_msg: 'Page created successfully',
      created_fail_msg: 'Page could not be created',
      mark_raw_image: 'Raw img',
      mark_raw_image_tooltip: 'Mark this page problematic, with the {{raw image}} template (for full-page images).',
      raw_image_summary: "[[Template:raw image]]",
      mark_without_text_tooltip: 'Mark this page as "without text" (for blank pages).',
      mark_table: 'Table',
      mark_table_tooltip: 'Mark this page problematic, with the {{missing table}} template (for full-page tables).',
      table_summary: '[[Template:Missing table]]',
      tool_name: "Index Preview",
      summary_note: "using [[$1|$2]]",

    },
    tool_link: "User:Inductiveload/index preview",
    tag_name: "index preview tool",
    toast_timeout: 4000,
    min_retry_timeout: 200,
    max_retry_timeout: 2000,
    popup_padding: 5,
    popup_img_size: 350,
    grid_img_size: 100,
    grid_gap: 6,
    thumb_rate_limit: 70 / 30,
    thumb_rate_margin: 1.5,
    access_key: 'G',
  };

  var window_manager;

  /**
   * Get the imageinfo for a given page and callback with it
   * @param  {[type]} img    the file name (without NS)
   * @param  {[type]} page   the page number
   * @param  {[type]} size   the requested size
   * @param  {[type]} iiprop the requested properties, |-separated
   * @param  {[type]} ii_cb  the function to call with the imageinfo
   */
  function get_imageinfo_for_page(img, page, size, iiprop, ii_cb) {

    var api = new mw.Api();

    api.get({
        'action': 'query',
        'prop': 'imageinfo',
        'titles': "File:" + img,
        'formatversion': 2,
        'format': 'json',
        'iiprop': iiprop,
        'iiurlparam': "page" + page + "-" + size + "px"
      })
      .done(function(data) {
        ii_cb(data.query.pages[0].imageinfo[0]);
      });
  }

  /*
   * Do an action if a specific page exists, or not
   */
  function if_page_exists(page, is_exists, pg_cb) {

    var api = new mw.Api();
    api.get({
        'action': 'query',
        'prop': 'pageprops',
        'titles': page,
        'formatversion': 2,
        'format': 'json',
      })
      .done(function(data) {
        var page = data.query.pages[0];
        if ((is_exists && page.missing === undefined)
           || (!is_exists && page.missing === true)) {
          pg_cb(page);
        }
      });
  }

  function update_labels(add) {

    var text, tt;
    if (add) {
      text = Preview.strings.show_grid;
      tt = Preview.strings.show_grid_tooltip;
    } else {
      text = Preview.strings.hide_grid;
      tt = Preview.strings.hide_grid_tooltip;
    }

    $(".userjs-prp-index-grid-trigger a")
      .text(text)
      .attr("title", tt);
  }


  function activate_grid() {

    if ($(".userjs-prp-index-grid").length > 0) {
      $(".userjs-prp-index-grid").remove();
      update_labels(true);
    } else {
      var filename = mw.config.get("wgTitle");

      // The reason we do this, rather than scraping the pagelist is that
      // the page list isn't guaranteed to have every single link in it
      // TODO: fall back to pagelist for non DJVU/PDF files
      get_imageinfo_for_page(filename, 1, 100, "dimensions|url", function(imageinfo) {
        build_grid(filename, imageinfo.thumburl, imageinfo.pagecount);
        update_labels(false);
      });
    }
  }

  function strip_ns(title) {
    return title.replace(/^[A-Za-z]+:/, "");
  }

  /*
   * Get the file and page number from a link
   */
  function get_file_from_pagelink($link) {
    const url = new URL($link.attr('href'), "https://" + mw.config.get("wgServer"));

    var splt = url.pathname.split("/");

    var parts = null;

    if (splt[1] === "wiki") {
      parts = [ strip_ns(splt[2]), splt[3] ];
    } else if (splt[1] === "w") {
      // redlink?
      var title = url.searchParams.get("title").split("/");
      parts = [ strip_ns(title[0]), title[1]];
    } else {
      console.error("Unknown URL structure: ", url);
    }

    return parts;
  }

  function get_random_integer(min, max) {
    return Math.floor(Math.random() * (max - min + 1) ) + min;
  }

  /*
   * Handler for clicks on page thumbnails
   * Alt: show a popup and queue an zoomed image load into it
   *
   * $attachment_pt: where the popup attaches
   * url_getter: function that find the url calls the given callback with that url
   */
  function page_thumb_click(event, page_title, $attachment_pt, url_getter) {
    if (!event.altKey) {
      return;
    }

    var progbar = new OO.ui.ProgressBarWidget({
      progress: false,
    });

    var popup = new OO.ui.PopupWidget({
      $floatableContainer: $attachment_pt,
      autoClose: true,
      align: 'center',
      classes: ["userjs-prp-page-preview-popup"],
      hideWhenOutOfView: false,
      data: {
        page_title: page_title,
        progbar: progbar,
      }
    });

    popup.setAnchorEdge("bottom");

    $('<div>')
      .addClass('userjs-prp-page-popup-controls')
      .appendTo(popup.$body);

    $('<div>')
      .addClass('userjs-prp-page-popup-image')
      .append(progbar.$element)
      .appendTo(popup.$body);

    // Append and display
    $(document.body).append(popup.$element);
    popup.toggle(true);

    url_getter(function(img_url) {
      load_img_into_popup(img_url, popup);
    });

    if_page_exists(page_title, false, function() {
      load_quick_tools_into_popup(popup);
    });
  }

  function load_img_into_popup(img_url, popup) {

    var img = $("<img>")
      .attr("src", img_url)
      .on('error', handle_load_error)
      .on("load", function() {

        popup.getData().progbar.toggle(false);

        popup.$body
          .find(".userjs-prp-page-popup-image")
          .append(img);

        var marg = popup.$body.outerWidth(true) - popup.$body.innerWidth() + Preview.popup_padding * 2;
        popup.setSize(img.width() + marg, null, true);
      }
    );
  }

  function page_tool_button(text, title, onclick) {
    var button = new OO.ui.ButtonWidget( {
        label: text,
        title: title,
        flags: 'progressive'
    });

    button.on('click', onclick);
    return button;
  }

  function load_quick_tools_into_popup(popup) {

    var wot_button = page_tool_button(
      Preview.strings.mark_without_text,
      Preview.strings.mark_without_text_tooltip,
      function() {
        mark_without_text(popup.getData().page_title);
      }
    );

    var raw_button = page_tool_button(
      Preview.strings.mark_raw_image,
      Preview.strings.mark_raw_image_tooltip,
      function() {
        mark_raw_image(popup.getData().page_title);
      }
    );

    var table_button = page_tool_button(
      Preview.strings.mark_table,
      Preview.strings.mark_table_tooltip,
      function() {
        mark_table(popup.getData().page_title);
      }
    );

    var btn_grp = new OO.ui.ButtonGroupWidget({
        items: [wot_button, raw_button, table_button]
    });

    popup.$body.find(".userjs-prp-page-popup-controls")
      .append(btn_grp.$element);
  }

  function construct_page_content(header, body, footer, quality) {
    return '<noinclude><pagequality level="' + quality + '" ' +
          'user="' + mw.config.get("wgUserName") + '" />' + header + '</noinclude>' +
          body + '<noinclude>' + footer + '</noinclude>';
  }


  function set_page_quality(pg_title, quality) {

    pg_title = pg_title.replace(/_/g, ' ');

    $('a[title="' + pg_title + '"], a[title^="' + pg_title + ' "], '+
        '.userjs-prp-index-grid-page[data-pg_title="' + pg_title + '"] .userjs-prp-index-grid-pageindex')
      .addClass("quality" + quality)
      .removeClass("new")
  }


  function create_page(pg_title, summary, header, body, footer, quality) {
    var content = construct_page_content(header, body, footer, quality);

    summary += "; " + Preview.strings.summary_note
      .replace("$1", Preview.tool_link)
      .replace("$2", Preview.strings.tool_name);

    var api = new mw.Api();
    api
      .create(pg_title,
        {
          summary: summary,
          tags: Preview.tag_name
        },
        content
      )
      .done(function() {
        show_toast('success', Preview.strings.created_ok_msg, pg_title);
        set_page_quality(pg_title, quality);
      })
      .fail(function(error, info) {
        show_message(Preview.strings.created_fail_msg,
            info.error.info + "\n" + pg_title);
      });
  }

  function show_message(title, msg, button_text) {
    var message_dialog = new OO.ui.MessageDialog();
    window_manager.addWindows([message_dialog]);

    window_manager.openWindow(message_dialog, {
      title: title,
      message: msg,
      actions: [
        {
          action: 'accept',
          label: button_text || "OK",
          flags: 'primary'
        }
      ]
    });
  }

  function show_toast(type, title, message) {

    var bubble_type = type || "info";

    mw.notify(message, {
      title: title,
      type: bubble_type,
      autoHideSeconds: Preview.toast_timeout
    });
  }

  /*
   * Mark a given page as a "raw image" page
   */
  function mark_raw_image(pg_title) {
    var summary = Preview.strings.problematic_summary +
        " " + Preview.strings.raw_image_summary;
    create_page(pg_title, summary,
      '', '{{raw image|' + strip_ns(pg_title) + '}}', '', QUALITY.PROBLEMATIC);
  }

  /*
   * Mark a given page as a "raw image" page
   */
  function mark_table(pg_title) {
    var summary = Preview.strings.problematic_summary +
        " " + Preview.strings.table_summary;
    create_page(pg_title, summary,
      '', '{{missing table}}', '', QUALITY.PROBLEMATIC);
  }

  /*
   * Mark a given page as "without text"
   */
  function mark_without_text(pg_title) {
    create_page(pg_title, Preview.strings.without_text_summary,
      '', '', '', QUALITY.WITHOUT_TEXT);
  }

  var reload_queue = [];
  var reload_timer = null;

  function check_reload_queue() {
    // slightly under the max rate limit once we hit the limiter
    var timer_interval = Preview.thumb_rate_margin * (1000 / Preview.thumb_rate_limit);

    if (reload_timer === null && reload_queue.length > 0) {
      reload_timer = setInterval(function() {
        var to_reload = reload_queue[0];
        reload_queue.shift();

        if (reload_queue.length === 0) {
          window,clearInterval(reload_timer);
          reload_timer = null;
        }

        to_reload.attr("src", to_reload.attr("src"));
      }, timer_interval);
    }
  }

  /*
   * If an image fails to load (probably because of a rate limit), ask again
   * in a short while.
   */
  function handle_load_error(event) {
    var $img = $(event.target);
    // console.log("Load error", event);


    reload_queue.push($img);
    // we really want the first images to load first
    reload_queue.sort(function(a, b) {
      return a.data('pg_num') - b.data('pg_num');
    });

    check_reload_queue();
  }

  /*
   * Copies prooreading status pagenum and href from an index-pagelist link
   */
  function assign_page_status_from_link($pg_div, $link) {
    var classes = ["quality0", "quality1", "quality2", "quality3", "quality4", "new"];

    var $targets = $pg_div.find(".userjs-prp-index-grid-pageindex");

    for (var i = 0; i < classes.length; ++i) {
      if ($link.hasClass(classes[i])) {
        $targets.addClass(classes[i]);
      }
    }

    // chop off the hidden 00's
    var children = $link[0].childNodes;
    var text = children[children.length - 1].nodeValue;

    $targets
      .append("&nbsp;—&nbsp;" + text)
      .attr("href", $link.attr("href"));

    // and assign to the image link
    $pg_div.find(".userjs-prp-index-grid-page a")
      .attr("href", $link.attr("href"));
  }


  function assign_page_status($pg_div) {

    var pg_num = $pg_div.data("pg_num");

    // first, see if we can sneak it out of the pagelist

    // look for existing pages with quality classes
    var selector = ".index-pagelist a[href$='/" + pg_num + "']";
    $(selector).each(function(idx, elem) {
      assign_page_status_from_link($pg_div, $(elem));
    });

    // and now for red links
    selector = ".index-pagelist a[href*='/" + pg_num + "&action']";
    $(selector).each(function(idx, elem) {
      assign_page_status_from_link($pg_div, $(elem));
    });
  }

  function build_grid(filename, url, num_pages) {

    $(".userjs-prp-index-grid").parents("tr").first().remove();

    var $tr = $("<tr>");
    var $grid = $("<div>")
      .addClass("userjs-prp-index-grid")
      .appendTo($tr);

    var $tbody = $(".index-pagelist").parents("tr").first().parent();

    $tbody.append($tr);

    var last_slash = url.lastIndexOf("/");
    var page_prefix = mw.config.get("wgServer")
        + mw.config.get("wgArticlePath").replace("$1", "Page:" + filename);

    for (var page = 1; page <= num_pages; ++page) {
      var page_url = url.replace("/page1-", "/page" + page + "-");

      var $page_div = $("<div>")
        .addClass("userjs-prp-index-grid-page")
        .attr( { 'data-pg_num': page } )
        .attr( { 'data-pg_title': "Page:" + filename + "/" + page } )
        .data('pg_num', page);

      var $num = $("<div>")
        .addClass("userjs-prp-index-grid-info")
        .append($("<a>")
          .addClass("userjs-prp-index-grid-pageindex")
          .append("#" + page))
        .appendTo($page_div);

      var $a = $("<a>")
        .addClass("popups_nopopup") // disable popups here
        .attr("href", page_prefix + "/" + page)
        .appendTo($page_div);

      var $img = $("<img>")
        .attr("src", page_url)
        .on('error', handle_load_error)
        .data('pg_num', page)
        .click(function(event) {

          var src = $(this).attr("src");

          var url_getter = function(cb) {
            var img_url = src.replace(/\d+px/, Preview.popup_img_size + "px");
            cb(img_url);
          };

          // no ES-6 let, so cheat and use a data attribute
          var page_title = "Page:" + filename + "/" + $(this).data('pg_num');

          page_thumb_click(event,
            page_title,
            $(event.target).parents(".userjs-prp-index-grid-page").first(),
            url_getter);
        })
        .appendTo($a);

      assign_page_status($page_div);

      $grid.append($page_div);
    }
  }

  /*
   * A pagelist item was clicked - look up the image URL and pop-up the image
   */
  function on_pagelist_click(event) {
    var $link_elem = $(event.target);
    var img_page = get_file_from_pagelink($link_elem);
    var size = Preview.popup_img_size;
    var page_name = "Page:" + img_page[0] + "/" + img_page[1];

    get_imageinfo_for_page(img_page[0], img_page[1], size, "url", function(imageinfo) {
      var url_getter = function(url_cb) {
        url_cb(imageinfo.thumburl);
      };

      page_thumb_click(event, page_name, $link_elem, url_getter);
    });
  }

  function add_portlet(cb) {
    var portlet = mw.util.addPortletLink(
      'p-tb',
      '#',
      '',
      't-show-grid',
      '',
      Preview.access_key || undefined,
    );

    $(portlet)
      .addClass("userjs-prp-index-grid-trigger")
      .click(function(e) {
        e.preventDefault();
        cb();
      });

      update_labels(true);
  }

  /*
   * Add a button at the top right of the pagelist
   * This is enWS specific and depends on the layout of
   * MediaWiki:Proofreadpage index template
   */
  function add_page_list_button($btn) {
    $btn.css({float: "right"});
    $(".index-pagelist").parents("td").first().find("> :first-child").prepend($btn);
  }

  function add_grid_button(cb) {

    var $link = $("<a>")
      .click(cb);

    var $span = $("<span>")
      .addClass("userjs-prp-index-grid-trigger")
      .append("[", $link, "]");

    add_page_list_button($span);
    update_labels(true);
  }

  function init_index_grid_gadget() {

    window_manager = new OO.ui.WindowManager();
    $('body').append(window_manager.$element);

    // console.log("Init Index Page Grid");
    var max_w = Preview.grid_img_size * 10 + Preview.grid_gap * 10;

    // Install CSS
    install_css('\
.userjs-prp-index-grid {\
  display: grid;\
  grid-gap: ' + Preview.grid_gap + 'px;\
  grid-template-columns: repeat(auto-fill, minmax(' + Preview.grid_img_size + 'px, 1fr));\
  max-width: ' + max_w + 'px;\
}\
\
.userjs-prp-index-grid-page {\
  text-align: center;\
  position: relative;\
  margin-bottom: 5px;\
}\
.userjs-prp-index-grid-info {\
  width:100%;\
}\
.userjs-prp-page-popup-image img,\
.userjs-prp-index-grid-page img {\
  max-width: 100%;\
  height: auto;\
  border: 1px solid lightgrey;\
}\
.userjs-prp-page-preview-popup .oo-ui-popupWidget-popup {\
  padding: ' + Preview.popup_padding + 'px;\
}\
.userjs-prp-page-popup-image {\
  margin-top: 5px;\
}\
.userjs-prp-page-popup-controls {\
  text-align: center;\
  font-size: 0.8rem;\
}\
');
    // install the pagelist previewer always
    $(".index-pagelist a")
      .click(on_pagelist_click);

    add_portlet(activate_grid);
    add_grid_button(activate_grid);
  }

  mw.hook( 'ext.gadget.index-preview-grid.config' ).fire( Preview );

  if (mw.config.get("wgCanonicalNamespace") === "Index") {
    mw.loader.using(['mediawiki.util', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows'], function() {
      init_index_grid_gadget();
    });
  }

}(jQuery, mediaWiki));