//
// 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(" — " + 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));