User:Inductiveload/quick access.js
Jump to navigation
Jump to search
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.
Code that you insert on this page could contain malicious content capable of compromising your account. If you are unsure whether code you are adding to this page is safe, you can ask at the central discussion page, Scriptorium. The code will be executed when previewing this page under some skins, including Monobook. You can in the interim if you wish to refresh the content sooner under another skin. |
This script seems to have a documentation page at User:Inductiveload/quick access. |
/*
* quick_access - mouseless access to useful things like toolbox links
* and editor command
*/
(function($, mw) {
"use strict";
var DEBUG = 0;
var INFO = 1;
var ERROR = 2;
// The config string, must match the above
var log_levels = ["debug", "info", "error"];
var QuickAccess = function() {
this.actions = [];
this.log_level = ERROR;
this.close_on_focus_loss = false;
};
QuickAccess.prototype.init = function() {
this.log("Init Quick Access");
this.install_portlet();
};
QuickAccess.prototype.log = function(level, ...logged) {
if (level >= this.log_level) {
console.log("QuickAccess: ", logged);
}
};
QuickAccess.prototype.install_portlet = function() {
var self = this;
var portlet = mw.util.addPortletLink(
'p-tb',
'#',
'QuickAccess',
't-quickaccess',
'QuickAccess to all toolbox actions'
);
$(portlet).click(function(e) {
e.preventDefault();
self.activate();
});
// install global key handler (avoids nasty scrolling that happens with
// access keys)
$(document).keydown(function(e) {
if (e.shiftKey && e.altKey && e.key === 'A') {
e.preventDefault();
self.activate();
}
});
this.log(DEBUG, "Portlet installed");
};
/**
* Instantiate and show the dialog
*/
QuickAccess.prototype.activate = function() {
/**
* Subclass of an OOjs Dialog used to present the QuickAccess UI
*/
function QAccessDialog(qaccess, config) {
this.qaccess = qaccess;
this.log = qaccess.log;
QAccessDialog.super.call(this, config);
}
OO.inheritClass(QAccessDialog, OO.ui.ProcessDialog);
QAccessDialog.static.name = 'quickaccessdialog';
QAccessDialog.static.title = 'QuickAccess';
QAccessDialog.escapable = true;
// Customize the initialize() function: This is where to add content to the dialog body and set up event handlers.
QAccessDialog.prototype.initialize = function() {
var self = this;
// inject style rules
var style = $(
"<style>\n"+
".gadget-quickaccess-match-item { font-size: larger; }\n" +
".gadget-quickaccess-match-selected { background-color: rgb(248,248,255); }\n" +
"\n" +
".gadget-quickaccess-match-cat { color: #888; font-size: smaller; }\n" +
".gadget-quickaccess-match-desc { color: #888; font-size: smaller; }\n" +
"</style>");
$('html > head').append(style);
// Call the parent method
QAccessDialog.super.prototype.initialize.call(this);
// Create and append a layout and some content.
this.content = new OO.ui.PanelLayout({
padded: true,
expanded: false
});
var input = $("<input>").attr('id', 'gadget-quickaccess-input')
.attr("placeholder",
"Enter command or toolbox item to execute")
.css({
clear: "both",
width: "90%"
});
var item_cntnr = $("<div>").attr("id",
"gadget-quickaccess-matches")
.css({
"margin-top": "10px",
"clear": "both"
});
var dialog = $("<div>").attr("id", "gadget-quickaccess-dialog")
// .css({})
.append(input, item_cntnr);
// Append the icon and label to the DOM
this.content.$element.append(dialog[0]);
this.$body.append(this.content.$element);
this.$body.on('focusout', function(evt) {
self.qaccess.log(DEBUG, "Focus lost, autoclosing");
self.close();
});
$('#gadget-quickaccess-input').on('keydown', function(evt) {
self.qaccess.log(DEBUG, "keydown", evt);
switch (evt.keyCode) {
case 13:
self.run_selected();
break;
case 38: // up
case 40: // down
self.move_sel(evt.keyCode === 38);
break;
default:
return; // exit this handler for other keys
}
evt.preventDefault(); // prevent the default action (scroll / move caret)
});
$('#gadget-quickaccess-input').on('input', function(evt) {
self.handle_input(evt);
});
};
QAccessDialog.static.actions = [{
action: 'cancel',
label: 'Cancel',
flags: ['safe', 'back']
}];
QAccessDialog.prototype.getActionProcess = function(action) {
var dialog = this;
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
};
QAccessDialog.prototype.getReadyProcess = function (data) {
var dialog = this;
return QAccessDialog.super.prototype.getReadyProcess.call( this, data )
.next( function () {
$('#gadget-quickaccess-input').focus();
}, this );
};
QAccessDialog.prototype.getBodyHeight = function() {
//can this not auto expand?
return 300;
};
QAccessDialog.prototype.getTeardownProcess = function(data) {
var self = this;
return QAccessDialog.super.prototype.getTeardownProcess.call(
this, data)
.first(function() {
// Perform any cleanup as needed
self.getManager().clearWindows();
}, this);
};
QAccessDialog.prototype.populate = function(match_item) {
// this.log(DEBUG, "Populating actions");
var self = this;
var match = $("<div>").addClass("gadget-quickaccess-match-item")
.data({
'item': match_item
});
var key_str = "";
if (match_item.key) {
key_str = "[" + match_item.key + "]";
}
var ks = $("<span>")
.addClass("gadget-quickaccess-match-key")
.css({
display: "inline-block",
width: "2em",
color: "#888"
})
.html(key_str);
var ns = $("<span>")
.addClass("gadget-quickaccess-match-name")
.html(match_item.name);
match.append(ks, ns);
if (match_item.desc) {
var ds = $("<span>")
.addClass("gadget-quickaccess-match-desc")
.html(": " + match_item.desc);
match.append(ds);
}
if (match_item.cat) {
var cs = $("<span>")
.addClass("gadget-quickaccess-match-cat")
.html(' — ' + match_item.cat);
match.append(cs);
}
// the click handler
match.click(function() {
self.set_sel($(this));
// run the selected item if posisble
self.run_selected();
});
$('#gadget-quickaccess-matches').append(match);
};
QAccessDialog.prototype.populate_from_stored = function() {
var self = this;
var old_used = self.qaccess.get_used_items();
var added_cnt = 0;
for (var i = 0; i < old_used.length; i++) {
// find the selectors in the new items and add them to the list when
// found (this means if the tools doesn't exist on the page, it won't be
// shown, and if the name has changed (eg Alerts (n)), it will be
// correcr
var curr_item = null;
// populate in order of last used, up to our limit
for (var j = 0; j < self.qaccess.actions.length && added_cnt <
max_items; j++) {
if (this.qaccess.used_item_matches(old_used[i], self.qaccess.actions[
j])) {
self.populate(self.qaccess.actions[j]);
added_cnt++;
}
}
}
// fill the remainder with the existing actions up to the limit
// could be dupes here, but really, who cares, it'll be wiped out when
// typing and the MRU enties are lost
for (var k = 0; k < self.qaccess.actions.length && added_cnt <
max_items; k++) {
self.populate(self.qaccess.actions[k]);
added_cnt++;
}
self.set_sel($(".gadget-quickaccess-match-item:first"));
self.updateSize();
};
QAccessDialog.prototype.handle_input = function(evt) {
var self = this;
var typed = $('#gadget-quickaccess-input').val();
var scores = [];
for (var i = 0; i < this.qaccess.actions.length; i++) {
var score = this.qaccess.get_score_for_action(typed, this.qaccess
.actions[i]);
// ignore neutral and failure
if (score > 0) {
scores.push([score, i]);
}
}
scores.sort(function(a, b) {
return b[0] - a[0];
});
scores = scores.slice(0, max_items);
// clear old junk
$('#gadget-quickaccess-matches').empty();
for (i = 0; i < scores.length; i++) {
self.populate(this.qaccess.actions[scores[i][1]]);
}
self.set_sel($(".gadget-quickaccess-match-item:first"));
self.updateSize();
};
/**
* Sets the given item to be the selected item
*/
QAccessDialog.prototype.set_sel = function(selected) {
$(".gadget-quickaccess-match-item")
.removeClass("gadget-quickaccess-match-selected");
selected.addClass("gadget-quickaccess-match-selected");
};
/**
* Move selection along the item list by the given number of steps
*/
QAccessDialog.prototype.move_sel = function(up) {
var items = $(".gadget-quickaccess-match-item");
// current index
var index = $('.gadget-quickaccess-match-selected').index();
if (index < 0)
index = 0;
else {
index += (up ? -1 : 1);
if (index < 0)
index = items.length - 1;
else if (index >= items.length)
index = 0;
}
// remove old item classes
items.removeClass("gadget-quickaccess-match-selected");
$(items[index])
.addClass("gadget-quickaccess-match-selected");
};
QAccessDialog.prototype.run_selected = function() {
var self = this;
var sel = $('.gadget-quickaccess-match-selected');
var item = $(sel[0]).data('item');
if (item !== undefined) {
self.close();
self.qaccess.execute_item(item);
}
};
var self = this;
var max_items = 10;
var closetitle = "Close";
var closetext = "Close";
// avoid invokation when dialog open
if ($("#gadget-quickaccess-dialog").length > 0) {
return;
}
// save the active element
this.activeElem = document.activeElement;
// Make the window.
var myAccessDialog = new QAccessDialog(self);
var action_load_promise = self.gather_actions();
action_load_promise.then(function() {
// init the list when we have the current items to compare against
myAccessDialog.populate_from_stored();
});
// Create and append a window manager, which will open and close the window.
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
// Add the window to the window manager using the addWindows() method.
windowManager.addWindows([myAccessDialog]);
// Open the window!
windowManager.openWindow(myAccessDialog);
// sneakily overwrite the dialog transition time - QuickAccess, not
// LeisurelyAccess!
$(windowManager.$element).find('.oo-ui-window-frame')
.css({
'transition': '75ms'
});
// focus the input in just a moment
// setTimeout(function() {
// $('#gadget-quickaccess-input').focus();
// }, 300);
};
/**
* Class that represents an actions and its description
*/
var Action = function() {
this.type = undefined; // for example "portlet link" or "editor tool"
this.name = undefined; // the presented name of the action
this.desc = undefined; // a longer description, probably a tooltip
this.key = undefined; // accesskey, if any
this.cat = undefined; //a category string (perhaps the portlet portal title)
};
/**
* Find a "match score" for a given item against a provided user string
*
* Can use various heuristics, but the simplest are "starts with" (strong)
* and "contains" (weaker)
*
* Zero score means neutral, negative is no match, more positive is a better
* match
*/
QuickAccess.prototype.get_score_for_action = function(typed, action) {
var haystack = action.name || "";
var raw_search = haystack.toLowerCase().indexOf(typed.toLowerCase());
if (raw_search === 0) {
// initial match
return 100;
} else if (raw_search > 0) {
// other substring
return 50;
}
// no match
return -1;
};
/**
* Executes a link, either following href ,if useful. or invoking
* the click handler if it looks like "#"
*/
QuickAccess.prototype.execute_link = function(link) {
if (link.attr('href') === "#") {
link.click();
} else {
window.location.href = link.attr('href');
}
};
QuickAccess.prototype.santise_title = function(title) {
return title.replace(/\[.*\]$/, ""); // strip keys
};
QuickAccess.prototype.get_unique_portlet_selector = function(link) {
var li = link.parents("li");
var id = li.attr('id');
// has a unique id
if (id) {
return "#" + id;
}
// the combination of the classes should be enough
var classes = li[0].className.split(/\s+/).join(".");
// add li element for extra awesome
return "li" + "." + classes;
};
/**
* Look at a singe portlet (nav/tab bar items and convert
* to an Action Item
*/
QuickAccess.prototype.get_action_from_portlet = function(portlet) {
this.log(DEBUG, "get_action_from_portlet: " + portlet);
var self = this;
var link = $(portlet).find("a");
var name = link.html();
var title = link.attr('title') || "";
title = this.santise_title(title);
var key = link.attr('accesskey') || "";
// this is a bit slow, coluld be better but is it noticable?
var cat = $(link).parents(".portal").children("h3").html();
var action = new Action();
action.name = name;
action.desc = title;
action.selector = self.get_unique_portlet_selector($(link));
action.key = key;
action.cat = cat;
action.type = "portlet";
return action;
};
QuickAccess.prototype.execute_item = function(item) {
if (item.selector) {
var target = $(item.selector + " a");
if (target.length === 1) {
this.execute_link(target);
} else {
this.log("No unique target for selector: " + item.selector);
}
} else {
this.log("Cannot execute item without selector: " + item.name);
}
// remember the item for next time
this.store_used_item(item);
// restore the previous focus
$('#wpTextbox1').focus();
};
/**
* Gather available actions and update the action list. Returns
* a promise - this allows us to call early, but only pick up
* results much later after user is typing without blocking the UI
*/
QuickAccess.prototype.gather_actions = function() {
this.log(DEBUG, "gather_actions");
var self = this;
var dfd = new $.Deferred();
setTimeout(function() {
// role = navigation includes toolboxen and the top-row tabs
var portal_items = $('.portal li, .vector-menu li');
self.log(DEBUG, "Portal items: ", portal_items);
var tmp_actions = [];
for (var i = 0; i < portal_items.length; i++) {
var action = self.get_action_from_portlet(portal_items[i]);
tmp_actions.push(action);
}
// move over all at once (need a lock? in JS? maybe not?)
self.actions = tmp_actions;
self.log(DEBUG, "Completed gather_actions", self.actions.length);
dfd.resolve("Completed action scan");
}, 0);
return dfd.promise();
};
/*
* Get previously used items, in order of used (most recent first)
*/
QuickAccess.prototype.get_used_items = function() {
var mru = JSON.parse(localStorage.getItem("gadget-quickaccess-used"));
// not going to sanitise this, worst case it's just junk and breaks the
// list
if (mru instanceof Array)
return mru;
return [];
};
/**
* Store an item in the used item list and trim dupes
*/
QuickAccess.prototype.store_used_item = function(item) {
var self = this;
var old = self.get_used_items();
// store enough to indentify this item on a different page
// type isn't neededd now but it keeps it clear
var to_store = {
type: item.type,
selector: item.selector
};
// now, remove any that match, we'll insert at the front for MRU
for (var i = old.length - 1; i >= 0; i--) {
if (this.used_item_matches(old[i], item)) {
old.splice(i, 1);
}
}
old.unshift(to_store);
localStorage.setItem("gadget-quickaccess-used", JSON.stringify(old));
};
QuickAccess.prototype.used_item_matches = function(used, item) {
return used.selector == item.selector &&
used.type == item.type;
};
mw.loader.using(['mediawiki.util', 'oojs-ui-core', 'oojs-ui-widgets',
'oojs-ui-windows'
],
function() {
var access = new QuickAccess();
access.init();
});
}(jQuery, mediaWiki));