// $Id: imdbweaver.user.js 781 2014-06-26 23:26:56Z Chris $
// -----------------------------------------------------------------------------
// This is a Greasemonkey user script.
// To use it, first install Greasemonkey: http://www.greasespot.net/
// Then restart Firefox and revisit this script
// From the Firefox menu select: Tools -> Install User Script
// Accept the default configuration and install
// Now whenever you visit imdb.com you will see extra functionality
// Documentation here: http://refactoror.net/greasemonkey/imdbWeaver/doc.html
// -----------------------------------------------------------------------------
// ==UserScript==
// @name IMDb Weaver
// @moniker iwvr
// @namespace http://refactoror.net/
// @description Enhances the content of imdb pages by correlating information from related pages.
// @version 0.3.8.2
// @author Chris Noe
// @include imdb.com/*
// @include *.imdb.com/*
//---------
// @exclude *.imdb.com/images/*
// @exclude *doubleclick*
// @exclude *google_afc*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_log
// @grant GM_xmlhttpRequest
// ==/UserScript==
var dm = new DomMonkey({
name : "IMDb Weaver"
,moniker : "iwvr"
,version : "0.3.8.2"
});
// The values listed here are the first-time-use defaults
// They have NO EFFECT once this script is installed.
prefs.config({
"ajaxOperationLimit": 10
,"all-topnav-isExpanded": true
,"highlightTitleTypes": true
,"firstMatchAccessKey": "A"
,"name-headshotMagnifier": true
,"name-ShowAge": true
,"name-ShowAgeAtTitleRelease": true
,"prefsMenuAccessKey": "P"
,"prefsMenuPosition": "BR"
,"prefsMenuVisible": true
,"removeAds": true
,"title-attributes": true
,"title-headshotMagnifier": true
,"title-headshotMagnification": 12
,"title-ShowAges": true
,"title-StartResearch": true
});
var titleHighlighers = {
Khaki: ["(V)"] // direct to video, no theatrical release
,Lavender: ["(TV)", "TV series", "TV mini-series", "TV episode"]
,Pink: ["(VG)"] // video game
};
// --------------- Page handlers ---------------
tryCatch(dm.metadata["moniker"], function () {
if (isStaticPageRequest()) {
// a request from a DocumentContainer object
log.debug("~~~~~ Static page request, no page processing: " + dm.xdoc.location.href);
// TBD: need to add "ajaxstatic" param to all URLs to prevent page graph recursion
dm.xdoc.foreachNode(
"//a[contains(@href, 'imdb.com')]",
function(a) {
a.href = ajaxstaticUrl(a.href);
log.debug("In " + dm.xdoc.location.href + " :: " + a.href);
}
);
return;
}
if (window != window.top) {
log.debug("subordinate window, no page processing: " + document.location.href);
return; // this is a subordinate window (iframe, etc)
}
if (dm.xdoc.location.href.match(/\/title\/tt\d+\/?/)) enhanceTitlePage();
else if (dm.xdoc.location.href.match(/\/name\/nm\d+\/?/)) enhanceNamePage();
else if (dm.xdoc.location.href.match(/\/find\?/)) enhanceFindPage();
else if (dm.xdoc.location.href.match(/\/updates\/history/)) enhanceUpdatePage();
else {
log.info("IMDb generic page");
extendImdbDocument(dm.xdoc);
enhanceImdbPage(dm.xdoc);
}
});
function isStaticPageRequest() {
return dm.xdoc.location.href.match("ajaxstatic");
}
var titleDoc;
var nameDoc;
// --------- Annoying intervening Ad pages ---------
function expediteAdPage()
{
var i = location.href.indexOf("dest=");
var redirUrl = "http:" + location.href.substring(i + 12);
log.info("Ad page, skipping immediately to: " + redirUrl);
// location.href = redirUrl;
}
// --------------- Title Page handler ---------------
function enhanceTitlePage()
{
log.info("IMDb Title page");
titleDoc = extendImdbTitleDocument(dm.xdoc);
enhanceImdbPage(dm.xdoc);
dispatchFeature("removeAds", function()
{
titleDoc.removeAds();
});
// Gallery Magnifier
// titleDoc.foreachNode("//div[@class='media_strip_thumbs']//img[contains(@src, '._V1._CR')]", function(img) {
// // substitute the larger image
// img.src = img.src.replace("_V1._CR75,0,300,300_SS90_.jpg", "_V1._SY400_SX600_.jpg");
// img.style.cssFloat = "none";
// var a = img.selectNode("ancestor::a[1]");
// var wrapper_div = a.wrapIn("div", {className: "iwvr_gallery_pic"} );
// wrapper_div.style.minWidth = img.clientWidth;
// });
// titleDoc.addStyle(
// "div.iwvr_gallery_pic:hover a img {\n"
// + " height: auto;\n"
// + " width: auto;\n"
// + " position: absolute;\n"
// + " margin-top: +100px;\n"
// + " margin-left: -25px;\n"
// + "}\n"
// );
// createImageMagnifiers(titleDoc, "_V1._CR", /_CR[0-9,_]*/, "_SX600_SY400_");
dispatchFeature("title-headshotMagnifier", function()
{
var mag = prefs.get("title-headshotMagnification") || 12;
var pixels = mag * 32;
var fileSfx = "_V1._SX" + pixels + "_SY" + pixels + "_";
createImageMagnifiers(titleDoc, "_V1._SX23_SY30_", /_SX\d+_SY\d+_/, fileSfx);
});
dispatchFeature("title-attributes", function()
{
addTitleAttrs();
});
// Display rating/runtime/language directly below title
function addTitleAttrs()
{
var titleAttrs = new Array();
var cert = titleDoc.getCertification();
if (cert != null) {
titleAttrs.push(cert);
}
var runtime = titleDoc.getRuntime();
if (runtime != null) {
var rt = runtime + " min";
if (runtime > 60) {
rt += " (" + formatHoursMinutes(runtime) + ")";
}
titleAttrs.push(rt);
}
var lang = titleDoc.getLanguage();
if (lang != null) {
titleAttrs.push(lang);
}
var languages = [];
titleDoc.foreachNode("//a[contains(@href,'/language/')]", function(lang_a) {
languages.push(lang_a.textContent);
});
titleAttrs.push(languages.join("/"));
var title_div = titleDoc.selectNodeNullable("//div[@id='tn15title']");
if (titleAttrs.length > 0) {
title_div.appendChildText(titleAttrs.join(", "));
}
}
function formatHoursMinutes(min)
{
var m = "0" + min % 60; // (extra leading zero)
var h = (min - m) / 60;
return h + ":" + m.substring(m.length - 2);
}
addCastToolbar();
function addCastToolbar()
{
var toolbar_span = titleDoc.createXElement("span", {id: "iwvr_casttoolbar"} );
var quotedTitle = '"' + titleDoc.getTitle() + '"';
dispatchFeature("title-ShowAges", function()
{
var showCastAges_button = titleDoc.createXElement("button", {id: "iwvr_showCastAges"} );
with (showCastAges_button) {
className = "iwvr_button";
textContent = "Show Ages";
title = "Show cast ages when " + quotedTitle + " was released in " + titleDoc.getTitleYear();
addEventListener('click', showCastAges, false);
}
with (toolbar_span) {
appendChildTextNbsp(3);
appendChild(showCastAges_button);
}
});
dispatchFeature("title-StartResearch", function()
{
var startResearch_button = titleDoc.createXElement("button", {id: "iwvr_startReasearch"} );
with (startResearch_button) {
className = "iwvr_button";
addEventListener('click', startResearch, false);
textContent = "Start Research...";
title = "Mine additional information about " + quotedTitle;
}
with (toolbar_span) {
appendChildTextNbsp(3);
appendChild(startResearch_button);
}
});
var cast_table = titleDoc.getCast_table();
if (cast_table != null) {
cast_table.prependSibling(toolbar_span);
}
}
function showCastAges(event)
{
// prevent second execution on same page
event.target.removeEventListener('click', showCastAges, false);
// process each cast member name
var nodeCount = 0;
var ajaxOperationLimit = prefs.get("ajaxOperationLimit");
titleDoc.foreachCastMember_tr
(
"//td[@class='nm']"
+ titleDoc.CAST_NAMES_A
+ "[not(following-sibling::span[@id='iwvr_age'])]",
function(a_name)
{
if (nodeCount < ajaxOperationLimit || ajaxOperationLimit == -1)
{
var titleDC = new DocumentContainer();
log.debug("%%%%% loadFromSameOrigin: " + a_name.href);
titleDC.loadFromSameOrigin(
// extract info from the cast member page
a_name.href,
function(doc) {
var nameDoc = extendImdbNameDocument(doc);
var titleYear = titleDoc.getTitleYear();
if (titleYear == null || titleYear == "") {
age = "?";
}
else {
var age = nameDoc.getAge(titleYear);
if (age == null)
age = "?";
}
// var dd_a = nameDoc.selectNodeNullable("//a[contains(@href, 'death_date=')]");
// if (dd_a)
// dd = " " + dd_a.textContent;
var age_span = titleDoc.createXElement("span", {id: "iwvr_age"} );
age_span.appendChildText(" (" + age + ")");
a_name.appendSibling(age_span);
}
);
}
nodeCount++;
}
)
event.target.textContent = "Show More Ages";
event.target.addEventListener('click', showCastAges, false);
}
function startResearch()
{
if (titleDoc.selectNodeNullable("//div[@id='iwvr_researchItems_dialog']")) {
return; // the dialog is already open
}
// make all names on page draggable
titleDoc.foreachNode(
titleDoc.CAST_NAMES_A,
function(name_a) {
name_a.className = "iwvr_researchable_item";
name_a.addEventListener('draggesture', addResearchItem, false);
}
);
function addResearchItem(event)
{
var original_item_a = event.target.parentNode;
var dragged_item_a = original_item_a.cloneNode(true);
var tip = titleDoc.selectNodeNullable("//*[@id='draggingTip']");
if (tip != null)
tip.remove();
var ul = titleDoc.selectNode("//ul[@id='iwvr_research_itemlist']");
var li = titleDoc.createXElement("li");
li.appendChild(dragged_item_a);
ul.appendChild(li);
original_item_a.className = null;
original_item_a.removeEventListener('draggesture', addResearchItem, false);
};
// present the Research dialog
var researchDialog = new DialogBox(titleDoc, "Research");
var researchDialog_div = researchDialog.createDialog(
"iwvr_researchItems",
"z-index: 999; position: fixed; bottom: 5px; right: 10px;",
{ OK: okResearch, Cancel: cancelResearch }
);
researchDialog.main_td.style.padding = "6px";
with (researchDialog_div)
{
style.fontSize = "10pt";
style.fontFamily = "Arial, Helvetica, sans-serif";
researchDialog_div.style.overflow = "auto";
var sel = titleDoc.createSelect(
"Research",
{ name: "researchmode" },
{ tic: "Titles in common" }, "tic");
appendChild(sel);
var scroll_div = titleDoc.createXElement("div");
with (scroll_div) {
with (style) {
border = "1px inset Black"; margin = 4; padding = 5;
width = 200; height = 60; overflow = "auto";
}
var items_ul = titleDoc.createXElement("ul", {id: "iwvr_research_itemlist"} );
with (items_ul.style) {
marginTop = "0px"; paddingTop = "0px";
marginLeft = "0px"; paddingLeft = "1em";
}
appendChild(items_ul);
var tip_div = titleDoc.createXElement("div");
tip_div.id = "draggingTip";
with (tip_div.style) { color = "Gray";
width = "100%"; textAlign = "center";
height = "85%"; verticalAlign = "middle";
}
tip_div.appendChildText("Drag Names Here");
appendChild(tip_div);
}
appendChild(scroll_div);
var includes_div = titleDoc.createTopicDiv("Include");
includes_div.style.borderColor = "DarkGreen";
// includes_div.style.backgroundColor = "#66CC33";
with (includes_div.contentElement)
{
appendChild(titleDoc.createCheckbox(
"Individual Episodes",
{id: "include-episodes"},
false
));
}
appendChild(includes_div);
var excludes_div = titleDoc.createTopicDiv("Exclude Categories");
excludes_div.style.borderColor = "#AA0000";
// excludes_div.style.backgroundColor = "#FF3300";
with (excludes_div.contentElement)
{
appendChild(titleDoc.createCheckbox(
"Self",
{id: "exclude-self", title: "Examples: Oprah, Letterman"},
true
));
appendChildElement("br");
appendChild(titleDoc.createCheckbox(
"Archive Footage",
{id: "exclude-archive-footage"},
true
));
}
appendChild(excludes_div);
}
}
function okResearch(doc)
{
var titleDoc = extendImdbTitleDocument(doc);
// get URLs of selected research items
var peopleUrls = new Array();
titleDoc.foreachNode("//ul[@id='iwvr_research_itemlist']//a", function(name_a) {
peopleUrls.push(name_a.href);
});
var includeEpisodes = titleDoc.selectNode("//input[@id='include-episodes']").checked;
var omitCategories = new Array();
if (titleDoc.selectNode("//input[@id='exclude-self']").checked)
omitCategories.push("self");
if (titleDoc.selectNode("//input[@id='exclude-archive-footage']").checked)
omitCategories.push("archive");
endResearch(titleDoc);
if (peopleUrls.length > 0) {
correlatePeople(peopleUrls, includeEpisodes, omitCategories);
}
}
function cancelResearch(doc)
{
var titleDoc = extendImdbTitleDocument(doc);
endResearch(titleDoc);
}
function endResearch(titleDoc)
{
// remove class="iwvr_researchable_item"
titleDoc.foreachNode(
titleDoc.CAST_NAMES_A,
function(name_a) {
name_a.className = null;
}
);
}
var DEFAULT_JOB_ABBREVS = {
"A": "Actor",
"A'": "Actress",
"D": "Director",
"P": "Producer",
"PD": "Production Designer",
"W": "Writer",
"C": "Composer",
"ST": "Soundtrack",
"AD": "Art Department",
"MC": "Miscellaneous Crew",
"S": "Self",
"Ar": "Archive Footage"
};
// gather credits for specified people and
// determine what titles they have in common
function correlatePeople(urlList, includeEpisodes, omitCatList)
{
// foreach (var User in Users)
// {
// <div class="action-time">[ActionSpan(User)]</div>
// if (User.IsAnonymous)
// {
// <div class="gravatar32">[RenderGravatar(User)]</div>
// <div class="details">[UserRepSpan(User)]<br/>[UserFlairSpan(User)]</div>
// }
// else
// {
// <div class="anon">anonymous</div>
// }
// }
var jobAbbrevs = new AbbreviationMap(DEFAULT_JOB_ABBREVS);
var jointCredits_a = new Array();
var nameSet_a = new Array();
withDocuments(
urlList, // gather credits from each person's page
function(doc) {
var nameDoc = extendImdbNameDocument(doc);
var name_a = nameDoc.getName_a();
nameSet_a.push(name_a);
jointCredits_a = jointCredits_a.concat(
nameDoc.getCredits_a( {imdbName_a: name_a}, includeEpisodes, omitCatList)
);
},
function(docList)
{
// sort combined credits (by url)
sortBy(jointCredits_a, ["href"] );
sortBy(nameSet_a, ["textContent"] );
var creditsInCommon_a = new Array();
var soloCreditCount = 0;
foreachGrouping(jointCredits_a, "href", function(creds_a)
{
if (creds_a.length < 2) {
soloCreditCount++;
return; // exclude where not in common with anybody else
}
var nameJobMap = new Array();
for (var c in creds_a) {
var jobList = jobAbbrevs.registerList(creds_a[c].categoryList);
nameJobMap[creds_a[c].propertySet.imdbName_a] = jobList;
}
var combo_a = creds_a[0].cloneNode(true);
combo_a.nameJobMap = nameJobMap;
creditsInCommon_a.push(combo_a);
});
// sort combined credits (by title)
sortBy(creditsInCommon_a, ["textContent"] );
// create information popup
var resultsWindow = new DialogBox(titleDoc, "Research Results");
var resultsWindow_div = resultsWindow.createDialog(
"iwvr_creditsInCommon",
"z-index: 999; position: fixed; left: 15px; top: 20px;",
{ X: noop }
);
resultsWindow.main_td.style.padding = "6px";
with (resultsWindow_div)
{
style.maxWidth = window.innerWidth - 70;
style.maxHeight = window.innerHeight - 90;
// style.overflow = "auto";
// construct the data table
var i = 1;
var table = titleDoc.createXElement("table", {className: "iwvr_results"} );
var caption = titleDoc.createXElement("caption");
caption.appendChildText("Credits in common", ["b"]);
table.appendChild(caption);
var cellAttrList = [
{className: "iwvr_results bulletcol"},
{className: "iwvr_results titlecol"}
].concat(
dup(nameSet_a.length, {className: "iwvr_results datacol"} )
);
var thead = titleDoc.createXElement("thead");
thead.appendTableRow([null, "Title"].concat(nameSet_a), cellAttrList);
table.appendChild(thead);
var tbody = titleDoc.createXElement("tbody");
for (var c in creditsInCommon_a)
{
var rowSet = initArrayIndices(2 + nameSet_a.length);
rowSet[0] = i + ")";
var credit_span = titleDoc.createXElement("span");
if (creditsInCommon_a[c].seriesTitle != null) {
credit_span.appendChildText(creditsInCommon_a[c].seriesTitle);
}
credit_span.appendChild(creditsInCommon_a[c]);
rowSet[1] = credit_span;
for (var name_a in creditsInCommon_a[c].nameJobMap) {
var jobList = creditsInCommon_a[c].nameJobMap[name_a];
var col = 2 + parseInt(arrayIndexOf(nameSet_a, name_a));
rowSet[col] = jobList.sort().join(", ");
}
tbody.appendTableRow(rowSet, cellAttrList);
i++;
}
var legend_div = jobAbbrevs.toLegend("div",
{ className: "iwvr_results_legend" } );
tbody.appendTableRow(
[ legend_div ],
[ { colSpan: (2 + nameSet_a.length) } ]
);
table.appendChild(tbody);
appendChild(table);
}
}
);
}
}
function dup(count, value) {
var returnValue = new Array();
for (var i = 0; i < count; i++) {
returnValue.push(value);
}
return returnValue;
}
// --------------- Name Page handler ---------------
function enhanceNamePage()
{
log.info("IMDb Name page");
var nameDoc = extendImdbNameDocument(dm.xdoc);
enhanceImdbPage(dm.xdoc);
dispatchFeature("removeAds", function()
{
nameDoc.removeAds();
});
dispatchFeature("name-headshotMagnifier", function()
{
createImageMagnifiers(nameDoc, "_V1._SX32_", /_SX\d+_/, "_SX640_SY720_");
});
var birthInfo_div = nameDoc.selectNodeNullable(
"//a[contains(@href, 'birth_year=')]/ancestor::div[1]");
var age = nameDoc.getAge();
var deathInfo_div = nameDoc.selectNodeNullable(
"//a[contains(@href, 'death_date=')]/ancestor::div[1]");
var ageDeath = nameDoc.getAgeDeath();
dispatchFeature("highlightTitleTypes", function()
{
// first defeat the site's regular even/odd row highlighting
nameDoc.foreachNode("//tbody[contains(@class, 'row-filmo-')]", function(tbody)
{
tbody.style.backgroundColor = "White";
});
// now add custom highlighting
highlightTitleTypes(titleHighlighers);
function highlightTitleTypes(hSpecs)
{
for (var color in hSpecs) {
var matchStrs = hSpecs[color];
for (var i in matchStrs) {
nameDoc.foreachNode([
"//text()[contains(., '" + matchStrs[i] + "')]//ancestor-or-self::tbody[1]",
"//text()[contains(., '" + matchStrs[i] + "')]//ancestor-or-self::li[1]"
],
function(item)
{
item.style.backgroundColor = color;
});
}
}
}
});
dispatchFeature("name-ShowAge", function()
{
if (birthInfo_div == null || age == null) {
log.info("name-ShowAge: no birth year");
}
else {
birthInfo_div.appendChildElement("br");
birthInfo_div.appendChildText(
((nameDoc.getDeathDetails_div() != null) ? "would be " : "is ")
+ age + " years old"
);
}
if (deathInfo_div == null || ageDeath == null) {
log.info("name-ShowAge: no death year");
}
else {
deathInfo_div.appendChildElement("br");
deathInfo_div.appendChildText(
"at age " + ageDeath
);
}
});
dispatchFeature("name-ShowAgeAtTitleRelease", function()
{
// if we are arriving at a person's page from a specific title page
if (dm.xdoc.referrer.match("imdb.com/title"))
{
// trim all but base title URL, (could be arriving from other detail pages)
dm.xdoc.referrer.match(/(.*tt\d*)/);
var referrerUrl = RegExp.$1;
var nameDC = new DocumentContainer();
nameDC.loadFromSameOrigin(
// use info from the referring title page
referrerUrl,
function(doc)
{
var titleDoc = extendImdbTitleDocument(doc);
var titleYear = titleDoc.getTitleYear();
var ageThen;
if (titleYear == null || titleYear == "") {
ageThen = "?";
}
else {
ageThen = nameDoc.getAge(titleYear);
if (ageThen == null)
ageThen = "?";
}
if (birthInfo_div == null || age == null) {
log.info("name-ShowAgeAtTitleRelease: no birth year");
return;
}
birthInfo_div.appendChildElement("br");
var t;
if (titleYear > (new Date()).getFullYear())
t = '(will be ' + ageThen
+ ' when "' + titleDoc.getTitle()
+ '" releases in ' + titleYear + ')'
;
else
t = '(was ' + ageThen
+ ' when "' + titleDoc.getTitle()
+ '" was released in ' + titleYear + ')'
;
birthInfo_div.appendChildText(t);
}
);
}
else {
if (dm.xdoc.referrer == "") {
log.warn("The name-ShowAgeAtTitleRelease option is enable"
+ " , but document.referrer is empty. REFERRER MAY BE DISABLED."
);
}
}
});
}
// --------------- Find Page handler ---------------
function enhanceFindPage()
{
log.info("IMDb Find page");
var findDoc = extendImdbNameDocument(dm.xdoc);
enhanceImdbPage(dm.xdoc);
// assign access key to first matching item
var accessKey = prefs.get("firstMatchAccessKey");
if (accessKey != null)
{
// first text link to "/rg/find-", that is inside a TD
var firstMatch_a = findDoc.selectNodeNullable(
"//td/a[not(img)][contains(@onclick, '/rg/find-')][1]");
if (firstMatch_a != null) {
firstMatch_a.accessKey = accessKey.toUpperCase();
var itemNum_td = firstMatch_a.selectNodeNullable("preceding::td[1]");
if (itemNum_td != null) {
var span = findDoc.createXElement("span");
span.appendChildText("[" + accessKey + "]");
firstMatch_a.href.match(/\/(\w+)\//);
var linkType = RegExp.$1; // "title" or "name"
span.title = "Alt-Shift-" + accessKey + " to go to this " + linkType;
span.style.fontWeight = "bold";
itemNum_td.innerHTML = "";
itemNum_td.appendChild(span);
}
}
}
dispatchFeature("highlightTitleTypes", function()
{
highlightTitleTypes(titleHighlighers);
function highlightTitleTypes(hSpecs)
{
for (var color in hSpecs) {
var matchStrs = hSpecs[color];
for (var i in matchStrs) {
findDoc.foreachNode("//a[contains(@onclick, '/rg/find-title')]/ancestor::table[1]//text()[contains(., '" + matchStrs[i] + "')]"
+ "//ancestor-or-self::td[1]", function(item)
{
item.style.backgroundColor = color;
});
}
}
}
});
// // Poster Magnifier
// findDoc.foreachNode("//a[contains(@href, 'title-tiny')]/img", function(img) {
// // substitute the larger image
// img.src = img.src.replace(/(\d)t\.(jpg|png|gif)$/, "$1m.$2");
// img.className = "iwvr_headshot";
// img.height = null;
// img.width = null;
// });
// titleDoc.addStyle(
// "img.iwvr_headshot { height: 32px; width: 22px; }\n"
// + "td:hover img.iwvr_headshot {\n"
// + " height: auto;\n"
// + " width: auto;\n"
// + " position: absolute;\n"
// + " margin-top: -59px;\n"
// + " margin-left: -125px;\n"
// + "}\n"
// );
}
// --------------- Find Page handler ---------------
function enhanceUpdatePage()
{
log.info("IMDb updates page");
// assign access key to first matching item
var accessKey = prefs.get("firstMatchAccessKey");
if (accessKey)
{
var updateDoc = extendDocument(dm.xdoc);
// first update item
var firstMatch_a = updateDoc.selectNodeNullable("(//a[contains(@href, 'update?load=')])[1]");
if (firstMatch_a) {
firstMatch_a.accessKey = accessKey.toUpperCase();
var item_td = firstMatch_a.selectNodeNullable("ancestor::td[1]");
if (item_td) {
var span = updateDoc.createXElement("span");
span.appendChildText("[" + accessKey + "]");
span.style.fontWeight = "bold";
item_td.prependChild(span);
}
}
}
}
// --------------- Find Page handler ---------------
function enhanceImdbPage(imdbDoc)
{
log.info("enhanceImdbPage");
imdbDoc.removeAds();
var navbar_div = imdbDoc.selectNodeNullable("//div[@id='nb20']");
if (navbar_div != null) {
navbar_div.makeCollapsible("all-topnav-isExpanded", true);
}
}
// --------------- Title Page extensions ---------------
function extendImdbTitleDocument(titleDoc)
{
if (titleDoc == null)
return null;
extendImdbDocument(titleDoc);
titleDoc.removeAds_super = titleDoc.removeAds;
titleDoc.removeAds = function()
{
this.removeAds_super();
titleDoc.foreachNode([
"//div[@id='tn15shopbox']"
,"//div[@id='tn15adrhs']"
,"//div[starts-with(@id, 'banner')]"
,"//div[@id='tn15tc']"
// ,"//a[contains(href, NAVSTRIP)]"
], function(node) {
node.remove();
log.debug("Removing ad element: <" + node.tagName + " id=" + node.id);
}
);
}
titleDoc.getTitle = function()
{
var title;
var poster_a = this.selectNodeNullable("//a[@name='poster']");
if (poster_a != null) {
title = poster_a.title;
}
else {
var title_span = this.selectNode("//title/text()");
title = title_span.textContent.replace(/\(\d\d\d\d\)/, "");
}
return title.stripQuoteMarks().normalizeWhitespace();
};
titleDoc.getTitle_a = function()
{
var a = this.createXElement("a");
a.href = this.location.href;
a.appendChildText(this.getTitle());
return a;
}
titleDoc.getTitleYear = function() {
var year;
var airDate = this.selectTextContent(
"//*[text()='Original Air Date:']/following-sibling::*[1]/text()");
if (airDate != null) {
airDate.match(/\d+ \w+ (\d\d\d\d)/);
year = RegExp.$1;
return year;
}
var year = this.selectTextContent(
"//div[@id='tn15title']//a[contains(@href, '/year/')]/text()");
return year;
};
titleDoc.getUserRating = function() {
var userRating = this.selectTextContent(
"//*[contains(text(), 'User Rating:')]/following-sibling::*[1]");
if (userRating == null)
return null;
userRating = userRating.split("/");
return userRating[0] / userRating[1];
};
titleDoc.getRuntime = function() {
var runtime_text = this.selectNodeNullable(
"//*[text()='Runtime:']/following-sibling::*[1]/text()");
if (runtime_text == null) {
return null;
}
var runtime = runtime_text.textContent.match(/(\d+)/);
return RegExp.$1;
};
titleDoc.getCertification = function(country) {
if (country == null) {
country = "USA";
}
var cert = this.selectTextContent(
"//a[starts-with(@href, concat('/List?certificates=', '" + country + ":'))]");
return cert;
};
titleDoc.getLanguage = function() {
var lang_a = this.selectNodeNullable(
"//a[starts-with(@href, '/Sections/Languages/')]");
if (lang_a == null) {
return null;
}
return lang_a.textContent;
};
titleDoc.CAST_TABLE =
"//table[@class='cast']"
;
titleDoc.CAST_NAMES_A = "//a[starts-with(@href, '/name/')]";
titleDoc.CHARACTER_NAME_A = "//a[@href='quotes']";
titleDoc.foreachCastMember_tr = function(relativeXpath, func)
{
this.foreachNode(
titleDoc.CAST_TABLE
+ "//tr"
+ relativeXpath,
func
);
}
titleDoc.getCast_table = function() {
return this.selectNodeNullable(titleDoc.CAST_TABLE);
};
log.debug(
"extendImdbTitleDocument: "
+ "title='" + titleDoc.getTitle() + "'"
+ ", titleYear=" + titleDoc.getTitleYear()
+ ", userRating=" + titleDoc.getUserRating()
);
return titleDoc;
}
// --------------- Title Page extensions ---------------
function extendImdbNameDocument(nameDoc)
{
if (nameDoc == null)
return null;
extendImdbDocument(nameDoc);
nameDoc.removeAds_super = nameDoc.removeAds;
nameDoc.removeAds = function()
{
this.removeAds_super();
var ad_div = nameDoc.selectNodeNullable("//div[@id='tn15adrhs']");
if (ad_div != null) {
ad_div.remove();
}
}
nameDoc.getName = function() {
return this.selectTextContent("//title");
};
nameDoc.getName_a = function()
{
var a = this.createXElement("a");
a.href = this.location.href;
a.appendChildText(this.getName());
return a;
}
nameDoc.getAge = function(refYear) {
if (refYear == null) {
refYear = (new Date()).getFullYear();
}
var birthYear = this.getBirthYear();
if (birthYear == null) {
return null;
}
var age = refYear - birthYear;
if (isNaN(age)) {
return "??";
}
else {
return age;
}
};
nameDoc.getBirthYear = function() {
var birthYear_a = this.selectNodeNullable(
"//a[contains(@href, 'birth_year=')]"
);
if (birthYear_a == null)
return null;
else
return birthYear_a.textContent;
};
nameDoc.getBirthDetails_end = function() {
var birthDetails_end = this.selectNodeNullable(
"//a[contains(@href, 'birth_year=')]"
+ "/following::br[1]"
);
return birthDetails_end;
}
nameDoc.getAgeDeath = function() {
var deathYear = this.getDeathYear();
if (deathYear == null) {
return null;
}
var ageDeath = deathYear - this.getBirthYear();
if (isNaN(ageDeath)) {
return "??";
}
else {
return ageDeath;
}
};
nameDoc.getDeathYear = function() {
var deathYear_a = this.selectNodeNullable(
"//a[contains(@href, 'death_date=')]"
);
if (deathYear_a == null)
return null;
else
return deathYear_a.textContent;
};
nameDoc.getDeathDetails_div = function() {
return this.selectNodeNullable(
"//*[contains(@href, 'death_date=')]"
+ "//ancestor::div[1]"
);
}
nameDoc.CREDIT_CATEGORIES_A =
"//a[starts-with(@href, '/title/')]"
+ "/ancestor::div[@class='filmo']/descendant::a[@name][1]";
// Get all the credits on this page, grouped by title.
// The list of job categories is attached to each "merged" title reference.
nameDoc.getCredits_a = function(propSet, includeEpisodes, omitCatList)
{
var creditList_a = new Array();
nameDoc.foreachNode( // Director, Writer, Actor, etc
nameDoc.CREDIT_CATEGORIES_A,
function (category_a)
{
var catLabel = category_a.textContent.replace(/:/, "");
if (arrayIndexOf(omitCatList, category_a.name))
return;
var matchCredits = "//a[@name='" + category_a.name + "']"
+ "/following::ol[1]/li/a[1]";
nameDoc.foreachNode( // each credit within a category
matchCredits,
function (credit_a) {
credit_a.category = catLabel;
if (propSet != null) {
credit_a.propertySet = propSet;
}
creditList_a.push(credit_a);
if (includeEpisodes) {
credit_a.foreachNode( // each sub-credit within a credit
"following-sibling::a",
function (subCredit_a) {
subCredit_a.category = catLabel;
if (propSet != null) {
subCredit_a.propertySet = propSet;
}
subCredit_a.seriesTitle = credit_a.textContent;
creditList_a.push(subCredit_a);
}
);
}
});
});
return mergeCreditCategories(creditList_a);
// for each title combine multiple credits into a single object
// with an array property that lists the category names
function mergeCreditCategories(creditList_a)
{
sortBy(creditList_a, ["href"] );
var mergedCredits = new Array();
foreachGrouping(creditList_a, "href", function(creds_a)
{
var catList = new Array();
for (var i in creds_a) {
catList.push(creds_a[i].category);
}
// reuse first element as a protoype
var combined_a = creds_a[0].cloneNode(true);
combined_a.textContent = combined_a.textContent.stripQuoteMarks();
combined_a.category = null;
combined_a.categoryList = catList;
combined_a.propertySet = creds_a[0].propertySet;
mergedCredits.push(combined_a);
});
return mergedCredits;
}
}
log.debug(
"extendImdbNameDocument: "
+ "name='" + nameDoc.getName() + "'"
+ ", birthYear='" + nameDoc.getBirthYear() + "'"
+ ", age=" + nameDoc.getAge()
);
return nameDoc;
}
// --------------- IMDb Page extensions ---------------
function extendImdbDocument(imdbDoc)
{
extendDocument(imdbDoc);
addPrefsButton();
imdbDoc.removeAds = function()
{
log.debug("imdbDoc.removeAds");
this.foreachNode("//div[starts-with(@id, 'swf_')]", function(node) {
node.remove();
log.debug("Removing ad element: <" + node.tagName + " id=" + node.id);
}
);
// log.debug("sweeping for DOUBLECLICK:");
// this.foreachNode("//img[contains(@src, 'doubleclick.net')]/ancestor::d[1]", function(node) {
// node.remove();
// log.debug("Removing DOUBLECLICK ad div");
// }
// );
this.removeFlashAds();
// experimental
// this.foreachNode("//area[@alt='Learn more']", function(div) {
// div.remove();
// log.debug("REMOVING NAVSTRIPE AD");
// }
// );
// this.foreachNode("//a[contains(@href, '_NAVSTRIPE')]//ancestor::div[1]", function(div) {
// div.remove();
// log.debug("REMOVING NAVSTRIPE AD");
// }
// );
this.foreachNode(
"//div[starts-with(@id, 'swf_')]",
function(ad_div) {
log.debug("REMOVING FLOATER AD");
ad_div.hideNode();
}
);
// this.removeFloaterAds();
// imdbDoc.onAppears("//div[starts-with(@id, 'swf_')]", 500, function(ad_div)
// {
// log.debug("Removing floater ad");
// alert("Removing floater ad");
// ad_div.hide();
// });
}
imdbDoc.removeFlashAds = function()
{
this.foreachNode(
"//comment()[contains(., 'FLASH AD BEGINS')]",
function(adbegin_cmt) {
// log.info("COMMENT< '" + adbegin_cmt.textContent + "'");
// var adend_cmt = adbegin_cmt.selectNodeNullable(
// "//following-sibling::comment()[contains(., 'FLASH AD ENDS')][1]");
// if (adend_cmt != null) {
// log.info("COMMENT> '" + adend_cmt.textContent + "'");
// }
var ad_div = adbegin_cmt.nextSibling.nextSibling;
if (ad_div != null) {
log.debug("Removing ad element: <" + ad_div.tagName + " id=" + ad_div.id);
ad_div.parentNode.removeChild(ad_div);
}
}
);
}
// buttons
imdbDoc.addStyle(
".iwvr_button {\n"
+ " font-size: 8pt;\n"
+ " font-family: Helvetica Narrow, sans-serif;\n"
+ "}\n"
);
// research items
imdbDoc.addStyle(
"a.iwvr_researchable_item { background-color: Gold; }\n"
+ "a.iwvr_researchable_item:hover { cursor: crosshair; }\n"
);
// research result grid styles
imdbDoc.addStyle(
".iwvr_results {\n"
+ " padding: 3;\n"
+ " font-family: Arial Narrow, Helvetica Narrow, sans-serif;\n"
+ " font-size: small;\n"
+ "}\n"
+ "table.iwvr_results {\n"
+ " border-collapse: collapse;\n"
+ " font-family: Arial, Helvetica, sans-serif;\n"
+ "}\n"
+ "td.iwvr_results {\n"
+ " border: 1px solid Gray;\n"
+ " padding: 3;\n"
+ "}\n"
+ "div.iwvr_results_legend {\n"
+ " max-width: 100%;\n"
+ " text-align: center;\n"
+ "}\n"
+ "td.bulletcol { text-align: right; }\n"
+ "td.titlecol { text-align: left; }\n"
+ "td.datacol { text-align: center; }\n"
);
return imdbDoc;
}
// --------------- helper functions ---------------
function createImageMagnifiers(theDoc, imgUrlContains, imgUrlRegex, imgUrlReplacement)
{
// Headshot Magnifier
theDoc.foreachNode("//img[contains(@src, '" + imgUrlContains + "')]", function(img) {
if (img.src.indexOf("addtiny.gif") != -1) {
return; // skip place-holders
}
// substitute the larger image
img.src = img.src.replace(imgUrlRegex, imgUrlReplacement);
img.className = "iwvr_headshot";
img.height = null;
img.width = null;
});
theDoc.addStyle(
"img.iwvr_headshot { height: 30px; width: 23px; }\n"
+ "td:hover img.iwvr_headshot {\n"
+ " height: auto;\n"
+ " width: auto;\n"
+ " z-index: 999;\n"
+ " position: fixed;\n"
+ " top: 5%;\n"
+ " right: 5%;\n"
+ "}\n"
);
// zoom visualization graphic
// var zoom_img = dm.xdoc.createXElement("img");
// with (zoom_img.style) {
// position = "absolute";
// top = "614px";
// left = "145px";
// }
// var zoom_img_src = 'data:image/gif;base64,' +
// 'R0lGODlhGACCAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/////' +
// '/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
// 'AAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBmZgBm' +
// 'mQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/mQD/zAD/' +
// '/zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNmzDNm/zOZADOZ' +
// 'MzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYA' +
// 'mWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZAGaZM2aZZmaZmWaZzGaZ' +
// '/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkz' +
// 'M5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnM' +
// 'mZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz' +
// '/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/' +
// 'M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9m' +
// 'mf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP//' +
// '/ywAAAAAGACCAAAI/wAfCBxIsKBBgggOKlSIIOHChwIbQoTY0OHEgxUvMsyosWBFix0jcgwpcmTIjyA7' +
// 'okx5cSXJBy5JrmRJMabKmSdx3tTZcibNjTwn+pTY06fGoURrDi26VGnTh0iTLowqFSNVp0ihUv1Z8ipQ' +
// 'r1a3Tt1adSDZsl3FGjyLli1NtyzhrpWLkK5ZuCDx5sVbl29atnf1/gUMU69Dw4cNDz67mGxjtYgfe41c' +
// 'WHBlv5QzK75MV7Nlz5g3g+4suvRn06FPq07N2m1gu5YlP+XM+HVt24773sYNlnfWubt9zxYeVHdv47/D' +
// 'HkdudOxy5sU9PoeOEmt04MOVX8e+XXpzodm/2i60bpI8WvHlzeesvv6jzPFMz2t1/z59/JeX8efHT/+l' +
// '/aPygaefSAPCVOCBDwQEADs=';
// zoom_img.src = zoom_img_src;
// titleDoc.body.appendChild(zoom_img);
// zoom_img.hide();
}
// ==================== AbbreviationMap object ====================
function AbbreviationMap(initMap)
{
this.map = initMap;
if (this.map == null) {
this.map = new Array();
}
this.register = function(value)
{
var abbrev = arrayIndexOf(this.map, value);
if (abbrev != null)
return abbrev;
for (var len = 1; len < value.length; len++)
{
var abbrev = value.substring(0, len);
if (this.map[abbrev] == null) {
this.map[abbrev] = value;
return abbrev;
}
}
throw "Can't abbreviate: '" + value + "'";
}
this.registerList = function(theList)
{
var abbrevList = new Array();
for (var i in theList) {
abbrevList.push(this.register(theList[i]));
}
return abbrevList;
}
this.toLegend = function(elemType, attrMap)
{
// var dm.xdoc = extendDocument(document);
var node = dm.xdoc.createXElement(elemType, attrMap);
with (node) {
for (var i in this.map) {
appendChildText(i);
appendChildText(':\u00A0"');
appendChildText(this.map[i]);
appendChildText('"');
appendChildText(' - ');
}
}
return node;
}
}
// ==================== Preferences Dialog ====================
function addPrefsButton()
{
configurePrefsButton(function(prefsMgr, prefsDialog_div)
{
var table = dm.xdoc.createXElement("table");
prefsDialog_div.appendChild(table);
var tr = dm.xdoc.createXElement("tr");
table.appendChild(tr);
var td = dm.xdoc.createXElement("td");
td.style.verticalAlign = "top";
tr.appendChild(td);
with (td)
{
var features_div = dm.xdoc.createTopicDiv("Enabled Features", td);
appendChild(features_div);
with (features_div.contentElement)
{
var genFeatures_div = dm.xdoc.createTopicDiv("All Pages", features_div);
appendChild(genFeatures_div);
with (genFeatures_div.contentElement)
{
appendChild(prefsMgr.createPreferenceInput(
"highlightTitleTypes",
"Highlight titles by type",
"white: theatrical release / gold: direct to video / blue: TV / pink: video game"
));
appendChildElement("br");
appendChild(prefsMgr.createPreferenceInput(
"removeAds",
"Remove advertising",
"Remove advertising"
));
}
var titleFeatures_div = dm.xdoc.createTopicDiv("On Title Pages", features_div);
appendChild(titleFeatures_div);
with (titleFeatures_div.contentElement)
{
appendChild(prefsMgr.createPreferenceInput(
"title-attributes",
"Display title attributes",
"Display rating/runtime/language directly below title"
));
appendChildElement("br");
appendChild(prefsMgr.createPreferenceInput(
"title-ShowAges",
"[Show Ages] button",
"Compute the ages of cast members"
));
appendChildElement("br");
appendChild(prefsMgr.createPreferenceInput(
"title-StartResearch",
"[Start Research] button",
"Open the Research dialog"
));
appendChildElement("br");
appendChild(prefsMgr.createPreferenceInput(
"title-headshotMagnifier",
"Headshot Magnifier",
"Hover mouse to magnify cast pictures"
));
appendChildElement("br");
appendChild(prefsMgr.createPreferenceInput(
"title-headshotMagnification",
"Mag level",
"Magnification factor",
{ size:2, maxLength: 2 }
)).style.marginLeft = "28px";
}
var nameFeatures_div = dm.xdoc.createTopicDiv("On Name Pages", features_div);
appendChild(nameFeatures_div);
with (nameFeatures_div.contentElement)
{
appendChild(prefsMgr.createPreferenceInput(
"name-ShowAge",
"Display age",
"Display current age of the person"
));
appendChildElement("br");
appendChild(prefsMgr.createPreferenceInput(
"name-ShowAgeAtTitleRelease",
"Display age at release",
"Display age at time title was released"
));
}
var findFeatures_div = dm.xdoc.createTopicDiv("On Search Results", features_div);
appendChild(findFeatures_div);
with (findFeatures_div.contentElement)
{
appendChild(prefsMgr.createPreferenceInput(
"firstMatchAccessKey",
"Select first match",
"Alt-Shift keyboard to navigate to first title matched",
{ size:1, maxLength: 1 }
));
}
}
}
var td = dm.xdoc.createXElement("td");
td.style.verticalAlign = "top";
tr.appendChild(td);
with (td) {
appendChild(prefsMgr.constructDockPrefsMenuSection(td));
appendChild(prefsMgr.constructAdvancedControlsSection(td));
var controls_div = dm.xdoc.createTopicDiv("Performance Controls", td);
with (controls_div.contentElement)
{
appendChild(prefsMgr.createPreferenceInput(
"ajaxOperationLimit",
"Background threads",
"Control how many simultaneous background operations are allowed"
+ ", (primarily affects Show Ages)",
{ size:1, maxLength: 2 }
));
}
appendChild(controls_div);
}
// Help link
var docs_div = dm.xdoc.createXElement("div");
prefsDialog_div.appendChild(docs_div);
with (docs_div) {
appendChild(dm.xdoc.createHtmlLink(
"http://refactoror.com/greasemonkey/imdbWeaver/doc.html#prefs",
"Help"
));
align = "center";
style.padding = "3px";
}
});
}
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-= refactoror lib -=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// common logic for the way I like to setup Preferences in my apps
// Requires preferences: prefsMenuAccessKey, prefsMenuPosition, prefsMenuVisible, loggerLevel
function configurePrefsButton(dialogConstructor)
{
// Preferences dialog
GM_registerMenuCommand(dm.metadata["name"] + " Preferences...", openPrefsDialog);
createPrefsButton();
// Prefs dialog
function createPrefsButton()
{
var menuButton = dm.xdoc.createXElement("button", { textContent: "Prefs" });
setScreenPosition(menuButton, prefs.get("prefsMenuPosition"));
if (prefs.get("prefsMenuVisible") == false) {
menuButton.style.opacity = 0; // active but not visibile
menuButton.style.zIndex = -1; // don't block other content
}
with (menuButton) {
id = dm.metadata["moniker"] + "_prefs_menu_button";
title = dm.metadata["name"] + " Preferences";
style.fontSize = "9pt";
addEventListener('click', openPrefsDialog, false);
// accessKey = getDeconflicted("prefsMenuAccessKey", "accessKey");
accessKey = prefs.get("prefsMenuAccessKey");
}
if (dm.xdoc.body != null) {
dm.xdoc.body.appendChild(menuButton);
}
}
function getDeconflicted(prefsName, attrName)
{
var prefValue = prefs.get(prefsName);
var node = xdoc.selectNodeNullable("//*[@" + attrName + "='" + prefValue + "']");
if (node != null) {
log.warn("Conflict: <" + node.nodeName + "> element on this page is already using "
+ attrName + "=" + prefValue);
prefValue = null;
}
return prefValue;
}
// Prefs dialog
function openPrefsDialog(event)
{
var prefsMgr = new PreferencesManager(
dm.xdoc,
dm.metadata["moniker"] + "_prefs",
dm.metadata["name"] + " Preferences",
{ OK: function okPrefs(doc) { prefsMgr.storePrefs(); },
Cancel: noop
}
);
var prefsDialog_div = prefsMgr.open();
if (prefsDialog_div == null)
return; // the dialog is already open
prefsMgr.constructDockPrefsMenuSection = function(contextNode)
{
var prefsDock_div = dm.xdoc.createTopicDiv("Dock [Prefs] Menu", contextNode);
contextNode.style.verticalAlign = "top";
with (prefsDock_div.contentElement)
{
appendChild(prefsMgr.createPreferenceInput(
"prefsMenuVisible",
"Visible",
"Prefs menu button visible on the screen"
));
with (appendChild(prefsMgr.createScreenCornerPreference("prefsMenuPosition"))) {
title = "Screen corner for [Prefs] menu button";
style.margin = "1px 0px 3px 20px";
}
appendChild(prefsMgr.createPreferenceInput(
"prefsMenuAccessKey",
"Access Key",
"Alt-Shift keyboard shortcut",
{ size:1, maxLength: 1 }
));
}
return prefsDock_div;
}
prefsMgr.constructAdvancedControlsSection = function(contextNode)
{
var controls_div = dm.xdoc.createTopicDiv("Advanced Controls", contextNode);
with (controls_div.contentElement)
{
appendChild(prefsMgr.createPreferenceInput(
"loggerLevel",
"Logging Level",
"Control level of information that appears in the Error Console",
null,
log.getLogLevelMap()
));
}
return controls_div;
}
dialogConstructor(prefsMgr, prefsDialog_div);
}
dispatchFeature("sendAnonymousStatistics", function() {
if (getElapsed("sendAnonymousStatistics") < 2000) {
log.debug("--------- SKIPPING COUNTER on rapid fire: " + dm.xdoc.location.href);
return;
}
log.debug("--------- EMBEDDING COUNTER: " + dm.xdoc.location.href);
// var counter_img = document.createElement("img");
// counter_img.id = "refactoror.net_counter";
// counter_img.src = "http://refactoror.net/spacer.gif?"
// + dm.metadata["moniker"] + "ver=" + dm.metadata["version"]
// + "&od=" + GM_getValue("odometer")
// ;
// log.debug(counter_img.src + " :: location=" + document.location.href);
// dm.xdoc.body.appendChild(counter_img);
});
function getElapsed(name) {
var prev_ms = parseInt(GM_getValue(name + "_ms", "0"));
var now_ms = Number(new Date());
GM_setValue(name + "_ms", now_ms.toString());
return (now_ms - prev_ms);
}
}
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-= DOM Monkey -=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
/* Parses the script headers into the metadata object.
* Adds constants & utility methods to various javascript objects.
* Initializes the Preferences object.
* Initializes the logger object.
*/
function DomMonkey(metadata)
{
extendJavascriptObjects();
// DM objects provided on the context
this.xdoc = extendDocument(document);
this.metadata = metadata;
// The values listed here are the first-time-use defaults
// They have no effect once they are stored as mozilla preferences.
prefs = new Preferences({
"loggerLevel": "WARN"
,"sendAnonymousStatistics": true
});
log = new Logger(this.metadata["version"]);
GM_setValue("odometer", GM_getValue("odometer", 0) + 1);
}
// ==================== DOM object extensions ====================
/** Extend the given document with methods
* for querying and modifying the document object.
*/
function extendDocument(doc)
{
if (doc == null)
return null;
/** Determine if the current document is empty.
*/
doc.isEmpty = function() {
return (this.body == null || this.body.childNodes.length == 0);
};
/** Report number of nodes that matach the given xpath expression.
*/
doc.countNodes = function(xpath) {
var n = 0;
this.foreachNode(xpath, function(node) {
n++;
});
return n;
};
/** Remove nodes that match the given xpath expression.
*/
doc.removeNodes = function(xpath) {
this.foreachNode(xpath, function(node) {
node.remove();
});
};
/** Hide nodes that match the given xpath expression.
*/
doc.hideNodes = function(xpath)
{
if (xpath instanceof Array) {
for (var xp in xpath) {
this.foreachNode(xp, function(node) {
node.hide();
});
}
}
else {
this.foreachNode(xpath, function(node) {
node.hide();
});
}
};
/** Make visible the nodes that match the given xpath expression.
*/
doc.showNodes = function(xpath) {
this.foreachNode(xpath, function(node) {
node.show();
});
};
/** Retrieve the value of the node that matches the given xpath expression.
*/
doc.selectValue = function(xpath, contextNode)
{
if (contextNode == null)
contextNode = this;
var result = this.evaluate(xpath, contextNode, null, XPathResult.ANY_TYPE, null);
var resultVal;
switch (result.resultType) {
case result.STRING_TYPE: resultVal = result.stringValue; break;
case result.NUMBER_TYPE: resultVal = result.numberValue; break;
case result.BOOLEAN_TYPE: resultVal = result.booleanValue; break;
default:
log.error("Unhandled value type: " + result.resultType);
}
return resultVal;
}
/** Select the first node that matches the given xpath expression.
* If none found, log warning and return null.
*/
doc.selectNode = function(xpath, contextNode)
{
var node = this.selectNodeNullable(xpath, contextNode);
if (node == null) {
// is it possible that the structure of this web page has changed?
log.warn("XPath returned no elements: " + xpath
+ "\n" + genStackTrace(arguments.callee)
);
}
return node;
}
/** Select the first node that matches the given xpath expression.
* If none found, return null.
*/
doc.selectNodeNullable = function(xpath, contextNode)
{
if (contextNode == null)
contextNode = this;
var resultNode = this.evaluate(
xpath, contextNode, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null);
if (resultNode.singleNodeValue == null)
log.debug("null result for: " + xpath);
return extendNode(resultNode.singleNodeValue);
}
/** Select all first nodes that match the given xpath expression.
* If none found, return an empty Array.
*/
doc.selectNodes = function(xpath, contextNode)
{
var nodeList = new Array();
this.foreachNode(xpath, function(n) { nodeList.push(n); }, contextNode);
return nodeList;
}
/** Select all nodes that match the given xpath expression.
* If none found, return null.
*/
doc.selectNodeSet = function(xpath, contextNode)
{
if (contextNode == null)
contextNode = this;
var nodeSet = this.evaluate(
xpath, contextNode, null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
return nodeSet;
}
/** Iteratively execute the given func for each node that matches the given xpath expression.
*/
doc.foreachNode = function(xpath, func, contextNode)
{
if (contextNode == null)
contextNode = this;
// if array of xpath strings, call recursively
if (xpath instanceof Array) {
for (var i=0; i < xpath.length; i++)
this.foreachNode(xpath[i], func, contextNode);
return;
}
var nodeSet = contextNode.selectNodeSet(xpath, contextNode);
var i = 0;
var n = nodeSet.snapshotItem(i);
while (n != null) {
var result = func(extendNode(n));
if (result == false) {
// dispatching func can abort the loop by returning false
return;
}
n = nodeSet.snapshotItem(++i);
}
}
/** Retrieve the text content of the node that matches the given xpath expression.
*/
doc.selectTextContent = function(xpath) {
var node = this.selectNodeNullable(xpath, this);
if (node == null)
return null;
return node.textContent.normalizeWhitespace();
};
/** Retrieve the text content of the node that matches the given xpath expression,
* and apply the given regular expression to it, returning the portion that matches.
*/
doc.selectMatchTextContent = function(xpath, regex) {
var text = this.selectTextContent(xpath);
if (text == null)
return null;
return text.match(regex);
};
/** Replace contents of contextNode (default: body), with specified node.
* (The specified node is removed, then re-added to the emptied contextNode.)
* The specified node is expected to be a descendent of the context node.
* Otherwise the result is probably an error.
* DOC-DEFAULT
*/
doc.isolateNode = function(xpath, contextNode)
{
if (contextNode == null)
contextNode = this.body;
extendNode(contextNode);
var subjectNode = this.selectNode(xpath);
if (subjectNode == null || subjectNode.parentNode == null)
return;
// gut the parent node (leave script elements alone)
contextNode.foreachNode("child::*", function(node) {
if (node.tagName != "SCRIPT" && node.tagName != "NOSCRIPT") {
node.remove();
}
});
// re-add the subject node
var replacement_div = this.createElement("div");
replacement_div.id = "isolateNode:" + xpath;
replacement_div.appendChild(subjectNode);
contextNode.appendChild(replacement_div);
return replacement_div;
};
/** Add a <script> reference to this document.
* DOC-CENTRIC
*/
doc.addScriptReference = function(url)
{
var script = this.createElement("script");
script.src = url;
this.selectNode("//head").appendChild(script);
return script;
}
/** Add a CSS style definition to this document.
* DOC-CENTRIC
*/
doc.addStyle = function(cssBody, id)
{
var style = this.createXElement("style");
style.innerHTML = cssBody;
this.selectNode("//head").appendChild(style);
return style;
}
/** Create an "extended" HTML element of the specified type,
* with the given attributes applied to it.
* The returned object is extended by extendNode().
* DOC-NONSPECIFIC
*/
doc.createXElement = function(tagName, attrMap)
{
var node = extendNode(this.createElement(tagName));
node.applyAttributes(attrMap);
return node;
}
/** Create
*/
doc.createHtmlLink = function(url, text, attrMap)
{
var a = this.createXElement("a");
a.href = url;
if (text == null) {
text = url;
}
a.textContent = text;
a.applyAttributes(attrMap);
return a;
}
/** Create an HTML input field, wrapped in an HTML label,
* with the given attributes applied to it,
* The returned HTML objects are extended by extendNode().
* DOC-NONSPECIFIC
*/
doc.createInputText = function(labelText, attrMap, defaultVal)
{
var span = this.createXElement("label");
with (span) {
if (labelText != null)
appendChildText(labelText + ": ");
var input = this.createXElement("input", attrMap);
with (input) {
type = "text";
value = defaultVal;
}
appendChild(input);
}
return span;
}
doc.createTextArea = function(labelText, attrMap, defaultVal)
{
var span = this.createXElement("label");
with (span) {
if (labelText != null)
appendChildText(labelText + ": ");
var input = this.createXElement("textarea", attrMap);
with (input) {
value = defaultVal;
}
appendChild(input);
}
return span;
}
/** Create an HTML checkbox, wrapped in an HTML label,
* with the given attributes applied to it,
* The returned HTML objects are extended by extendNode().
* DOC-NONSPECIFIC
*/
doc.createCheckbox = function(labelText, attrMap, isChecked)
{
var span = this.createXElement("label");
with (span) {
var input = this.createXElement("input", attrMap);
with (input) {
type = "checkbox";
checked = isChecked;
}
appendChild(input);
appendChildText(labelText);
}
return span;
}
/** Create a set of HTML radio buttons, wrapped in an HTML label element.
* The returned HTML objects are extended by extendNode().
* DOC-NONSPECIFIC
*/
doc.createRadioset = function(attrMap, optionMap, defaultKey)
{
var spanList = new Array();
for (var key in optionMap)
{
var label = this.createXElement("label");
with (label) {
var input = this.createXElement("input", attrMap);
with (input) {
type = "radio";
value = key;
if (key == defaultKey)
checked = true;
}
appendChild(input);
appendChildText(optionMap[key]);
}
spanList.push(label);
}
return spanList;
}
/** Create an HTML select element, wrapped in an HTML label element.
* The returned HTML objects are extended by extendNode().
* DOC-NONSPECIFIC
*/
doc.createSelect = function(labelText, attrMap, optionMap, defaultKey)
{
var span = this.createXElement("label");
with (span) {
if (labelText != null)
appendChildText(labelText + ": ");
var select = this.createXElement("select", attrMap);
with (select)
{
for (var key in optionMap)
{
var option = this.createXElement("option");
with (option) {
value = key;
if (key == defaultKey) {
selected = true;
}
appendChildText(optionMap[key]);
}
appendChild(option);
}
}
appendChild(select);
}
return span;
}
/** Create a labeled/boxed area (eg, typical dialog box component).
*/
doc.createTopicDiv = function(topicTitle, contextNode)
{
var shiftEms = ".7";
var basecolor = getBaseColor(contextNode);
var frame_div = this.createXElement("div");
with (frame_div) {
with (style) {
border = "1px solid Gray";
marginTop = (shiftEms * 1.5) + "em";
marginLeft = "6px";
marginRight = "6px";
MozBorderRadius = "3px";
}
// superimposed title
var title_span = this.createXElement("span");
with (title_span.style) {
position = "relative";
top = -shiftEms + "em";
fontSize = "10pt";
color = "Black";
backgroundColor = basecolor;
marginLeft = "6px"; // shift title right
padding = "0px 4px 0px 4px"; // blot out frame on left & right
}
title_span.appendChildText(topicTitle);
appendChild(title_span);
// maintatin default mouse cursor over the topic label text
title_span.wrapIn("label");
// content area
var content_div = this.createXElement("div");
content_div.style.marginTop = -shiftEms + "em";
content_div.style.padding = "6px";
appendChild(content_div);
}
frame_div.contentElement = content_div;
return frame_div;
function getBaseColor(contextNode)
{
while (contextNode != null && contextNode.tagName != "BODY") {
var c = contextNode.style.backgroundColor;
if (c != "") {
return c;
}
contextNode = contextNode.parentNode;
}
return "White";
}
}
return doc;
}
/** Extend the given node with methods
* for querying and modifying the node object.
*/
function extendNode(node)
{
if (node == null)
return null;
/** Create an HTML element of the specified type,
* with the given attributes applied to it.
* The returned object is extended by extendNode().
*/
node.createXElement = function(tagName, attrMap)
{
var node = extendNode(this.ownerDocument.createElement(tagName));
this.applyAttributes(attrMap);
return node;
}
// Selection methods that operate within the scope of this node
node.selectValue = function(xpath) { return document.selectValue(xpath, this); }
node.selectNode = function(xpath) { return document.selectNode(xpath, this); }
node.selectNodeNullable = function(xpath) { return document.selectNodeNullable(xpath, this); }
node.selectNodeSet = function(xpath) { return document.selectNodeSet(xpath, this); }
node.foreachNode = function(xpath, func) { document.foreachNode(xpath, func, this); }
node.isolateNode = function(xpath) { document.isolateNode(xpath, this); }
node.applyAttributes = function(attrMap) {
for (var key in attrMap) {
this[key] = attrMap[key];
}
}
/**
*/
node.NBSP = "\u00A0";
/** Create a DOM object of the given type,
* and append it to this node.
*/
node.appendChildElement = function(tagName) {
var newNode = this.createXElement(tagName);
this.appendChild(newNode);
return newNode;
};
/** Create a text node,
* optionally wrapped in the given HTML element types,
* and append it to this node.
*/
node.appendChildText = function(text, spanList, attrMap)
{
var newNode = this.ownerDocument.createTextNode(text);
// wrap with other elements, if any, (eg: ["b", "i"])
if (spanList != null) {
for (var i = spanList.length - 1; i >= 0; i--) {
var n = this.createXElement(spanList[i]);
n.appendChild(newNode);
newNode = n;
}
}
if (attrMap != null) {
newNode.applyAttributes(attrMap);
}
this.appendChild(newNode);
return newNode;
};
/** Create a text node consisting of a series of entities,
* and append it to this node.
*/
node.appendChildTextNbsp = function(count) {
if (count == null)
count = 1;
var buf = "";
for (var i = 0; i < count; i++) {
buf += this.NBSP;
}
return this.appendChildText(buf);
};
/** Insert the given node as the first child of this node.
*/
node.prependChild = function(newNode) {
this.insertBefore(newNode, this.firstChild);
return newNode;
};
/** Insert the given node in front of this node.
*/
node.prependSibling = function(newNode) {
var p = this.parentNode;
p.insertBefore(newNode, this);
return newNode;
};
/** Insert the given node after this node.
*/
node.appendSibling = function(newNode) {
var p = this.parentNode;
var followingSibling = this.nextSibling;
p.insertBefore(newNode, followingSibling);
return newNode;
};
/** Create an HTML element of the specified type,
* with the given attributes applied to it,
* then move this node inside the newly created node,
* and attach the newly created node in place of this node
* returning the newly created object.
*/
node.wrapIn = function(type, attrs) {
var wrapperNode = this.createXElement(type, attrs);
this.prependSibling(wrapperNode);
this.remove();
wrapperNode.appendChild(this);
return wrapperNode;
};
/**
*/
node.makeCollapsible = function(id, isPersistent, isInitExpanded) {
return new Collapsible(this, id, isPersistent, isInitExpanded);
};
/** Remove this node, and insert the given node in its place.
* .. more details
*/
node.replaceWith = function(node) {
this.appendSibling(node);
this.remove();
return node;
};
/** Create an HTML table row.
* .. more details
*/
node.appendTableRow = function(valueList, tdAttrMapList)
{
var tr = this.createXElement("tr");
for (var i in valueList)
{
var td = this.createXElement("td");
if (tdAttrMapList != null)
td.applyAttributes(tdAttrMapList[i]);
if (valueList[i] == null)
;
else if (typeof(valueList[i]) == "string")
td.appendChild( this.ownerDocument.createTextNode(valueList[i]) );
else
td.appendChild( valueList[i] );
tr.appendChild(td);
}
this.appendChild(tr);
}
/** Remove this node from the DOM.
*/
node.remove = function() {
this.parentNode.removeChild(this);
return this;
}
/** Hide this node.
*/
node.hide = function() {
this.style.display = "none";
}
/** Hide nodes that are siblings to this node.
*/
node.hideSiblings = function() {
this.foreachNode("../child::*", function(node) {
if (! this.isSameNode(node)) {
if (node.tagName != "SCRIPT" && node.tagName != "NOSCRIPT")
node.hide();
}
});
};
/** Show this node.
*/
node.show = function() {
this.style.display = null;
}
/** Calculate the absolute X position of this HTML element.
*/
node.findPosX = function()
{
var x = 0;
var node = this;
while (node.offsetParent != null) {
x += node.offsetLeft;
node = node.offsetParent;
}
if (node.x != null)
x += node.x;
return x;
}
/** Calculate the absolute Y position of this HTML element.
*/
node.findPosY = function()
{
var y = 0;
var node = this;
while (node.offsetParent != null) {
y += node.offsetTop;
node = node.offsetParent;
}
if (node.y != null)
y += node.y;
return y;
}
return node;
}
// ==================== TabSet object ====================
var activeTabsets = new Array();
// assumes that doc has already been extended
function TabSet(doc, tabsetId, tabLabels)
{
this.doc = doc;
this.tabsetId = tabsetId;
this.tabLinkMap = new Array();
this.tabDivMap = new Array();
// save TabSet object reference for callbacks
activeTabsets[tabsetId] = this;
this.getTabContent_div = function(labelText) {
return this.tabDivMap[labelText];
}
this.createTab = function(idx, labelText)
{
var a = this.doc.createXElement("a", {
name: this.tabsetId,
textContent: labelText,
className: "DialogBox_clickable"
});
with (a.style) {
padding = "3px 4px";
border = "1px solid Black";
MozBorderRadius = "4px";
borderBottom = "none";
fontSize = "9pt";
color = "Black";
textDecoration = "none";
}
return a;
}
this.activateTab = function(a) {
with (a.style) {
paddingTop = "4px";
backgroundColor = "LightGray";
}
var content_div = this.getTabContent_div(a.textContent);
content_div.show();
}
this.deactivateTab = function(a) {
with (a.style) {
paddingTop = "3px";
backgroundColor = "DarkGray";
}
var content_div = this.getTabContent_div(a.textContent);
content_div.hide();
}
this.selectTab = function(selected_a)
{
// (can be called from outside this object's context, (ie, click listener))
var tabset = activeTabsets[selected_a.name];
// deselect all tabs
tabset.doc.foreachNode("//a[@name='" + selected_a.name + "']", function(a) {
tabset.deactivateTab(a);
});
// then select the clicked tab
tabset.activateTab(selected_a);
}
this.initialize = function(labelText)
{
var maxX = 0;
var maxY = 0;
// determine largest width/height across content divs
for (var d in this.tabDivMap) {
var div = this.tabDivMap[d];
if (div.clientWidth > maxX) maxX = div.clientWidth;
if (div.clientHeight > maxY) maxY = div.clientHeight;
}
// equalize size of content divs to largest
for (var d in this.tabDivMap) {
var div = this.tabDivMap[d];
div.style.width = maxX;
div.style.height = maxY;
}
// select the default tab
if (labelText == null) {
labelText = tabLabels[0];
}
this.selectTab(this.tabLinkMap[labelText])
}
this.container_div = this.doc.createXElement("div", { id: this.tabsetId });
var ul = this.doc.createXElement("ul");
this.container_div.appendChild(ul);
with (ul.style) {
margin = "13px 7px 1px 12px";
padding = "0px 0px 0px 0px";
fontSize = "10pt";
}
for (var t in tabLabels)
{
var tab_a = this.createTab(t, tabLabels[t]);
tab_a.addEventListener('click', function(event) {
// now we're in the isolated context of the click
// ie, context inferred from event & globals
var selected_a = event.target;
var tabset = activeTabsets[selected_a.name];
tabset.selectTab(selected_a);
},
false
);
ul.appendChild(tab_a);
// maintatin default mouse cursor over the topic label text
tab_a.wrapIn("label");
this.tabLinkMap[tabLabels[t]] = tab_a;
// corresponding content div
var tabContent_div = this.doc.createXElement("div", {
id: this.tabsetId + ":" + tabLabels[t]
});
with (tabContent_div.style) {
margin = "0px 7px 0px 7px";
padding = "4px 4px 4px 4px";
border = "2px outset Black";
}
this.container_div.appendChild(tabContent_div);
this.tabDivMap[tabLabels[t]] = tabContent_div;
}
}
// ==================== DialogBox object ====================
var activeDialogs = new Array();
// assumes that doc has already been extended
function DialogBox(doc, dialogTitle)
{
this.doc = doc;
this.callbacks = null;
this.createDialog = function(popupName, dialogStyle, buttonDefs)
{
this.popupId = popupName + "_dialog";
var main_div = this.doc.createXElement("div");
with (main_div) {
id = this.popupId;
setAttribute("style", dialogStyle);
style.maxWidth = window.innerWidth - 50;
style.maxHeight = window.innerHeight - 70;
style.overflow = "auto";
if (style.backgroundColor == "")
style.backgroundColor = "White";
// dialog box structure
innerHTML =
// border layers
'<div style="border: 1px solid; border-color: Gainsboro DarkSlateGray DarkSlateGray Gainsboro;">'
+ '<div style="border: 1px solid; border-color: White DarkGray DarkGray White;">'
+ '<div style="border: 2px solid Gainsboro;">'
// grid (has to be a table to acheive float behaviors)
+ '<table cellspacing="0" cellpadding="0">'
+ '<tbody>'
// titlebar (optional)
+ ((dialogTitle != null) ?
'<tr id="' + this.popupId + '_titlebar"><td'
+ ' style="padding: 2px; background-color: Navy; color: White; font: bold 9pt Arial;"'
+ '>' + dialogTitle
+ '</td></tr>'
: "")
// main content area
+ '<tr id="' + this.popupId + '_main" style="overflow: auto;"><td>'
+ '<div id="' + this.popupId + '_content"/>'
+ '</td></tr>'
// button bar
+ '<tr id="' + this.popupId + '_buttons"><td style="padding: 6px;">'
+ '</td></tr>'
+ '</tbody>'
+ '</table>'
+ '</div>'
+ '</div>'
+ '</div>'
;
}
this.doc.body.appendChild(main_div);
// $(main_div).addClass("ui-widget-content ui-draggable");
// $(main_div).draggable();
this.main_td = main_div.selectNodeNullable("//tr[@id='" + this.popupId + "_main']/td")
var content_div = main_div.selectNode("//div[@id='" + this.popupId + "_content']");
var buttonbar_td = main_div.selectNodeNullable("//tr[@id='" + this.popupId + "_buttons']/td")
var controlButtons_span = this.doc.createXElement("center");
if (buttonDefs != null)
{
this.callbacks = buttonDefs;
for (var b in buttonDefs)
{
var button = null;
if (b == "X")
{
var titlebar_td = main_div.selectNodeNullable("//tr[@id='" + this.popupId + "_titlebar']/td")
if (titlebar_td != null) {
// X close button in the right side of the titlebar
button = this.doc.createXElement("a");
with (button) {
id = this.popupId + "_closer";
href = "javascript:void(0)";
with (style) {
cssFloat = "right";
border = "1px solid";
borderColor = "White DarkSlateGray DarkSlateGray White";
backgroundColor = "LightGray";
padding = "0px 1px 0px 2px";
font = "bold 9pt Arial";
color = "Black";
textAlign = "center";
lineHeight = "110%";
}
appendChildText("X");
}
titlebar_td.prependChild(button);
}
else {
// X close button in the upper-right of window
button = this.doc.createXElement("a");
with (button) {
id = this.popupId + "_closer";
href = "javascript:void(0)";
with (style) {
cssFloat = "right";
backgroundColor = "#AA0000";
padding = "2px";
font = "bold 8pt Arial";
textDecoration = "none";
color = "White";
}
appendChildText("X");
}
content_div.prependSibling(button);
}
}
else {
// a regular button at bottom of window
button = this.doc.createXElement("button");
with (button.style) {
margin = "0px 5px";
fontSize = "8pt";
fontFamily = "Helvetica, sans-serif";
}
controlButtons_span.appendChild(button);
}
with (button) {
name = this.popupId; // name attr associates callbacks with the dialog id
className = "DialogBox_clickable";
textContent = b;
addEventListener('click', function(event) {
// now we're in the isolated context of the click
// ie, context inferred from event & globals
var doc = extendDocument(event.target.ownerDocument);
var dialog = activeDialogs[event.target.name];
var popupId = event.target.textContent;
var callbackFunc = dialog.callbacks[popupId];
dialog.hidePopup();
callbackFunc(doc);
dialog.removePopup();
},
false
);
}
}
buttonbar_td.appendChild(controlButtons_span);
this.doc.addStyle(
".DialogBox_clickable:hover { cursor: pointer; }\n"
);
}
// save DialogBox object reference for callbacks
activeDialogs[this.popupId] = this;
return content_div;
}
this.hidePopup = function() {
var div = this.doc.getElementById(this.popupId);
div.style.display = "none";
}
this.removePopup = function() {
var div = this.doc.getElementById(this.popupId);
div.parentNode.removeChild(div);
activeDialogs[this.popupId] = null;
}
}
function noop() {
}
// ==================== Preferences object ====================
/** (This object is created before the Logger object,
* therefore the log methods cannot be used. Use GM_log instead.)
*/
function Preferences(defaultValuesMap)
{
this.defaultValuesMap = defaultValuesMap;
this.cacheMap = new Object();
/** Adds additional attributes to the map.
*/
// TBD: rename (add, merge)
this.config = function(valuesMap) {
for (var k in valuesMap) {
this.defaultValuesMap[k] = valuesMap[k];
}
}
this.get = function(prefName)
{
var value = this.cacheMap[prefName];
if (typeof(value) == "undefined")
{
value = GM_getValue(prefName);
if (typeof(value) == "undefined")
{
value = this.defaultValuesMap[prefName];
if (typeof(value) == "undefined") {
GM_log("Unmanaged preference: " + prefName);
return value;
}
}
this.set(prefName, value);
}
return value;
}
this.set = function(prefName, prefValue)
{
GM_setValue(prefName, prefValue);
this.cacheMap[prefName] = prefValue;
}
this.getAsList = function(prefName, delim, wrapperType)
{
var value = this.get(prefName);
var valueList;
if (value != null) {
valueList = value.split(delim);
}
else {
valueList = new Array();
}
if (wrapperType != null) {
// wrap elements in custom object type
var wrappedValueList = new Array();
for (var i=0; i < valueList.length; i++) {
wrappedValueList[i] = new wrapperType(valueList[i]);
}
return wrappedValueList;
}
// add utility methods to the resulting Array object
valueList.contains = function(matchText)
{
if (matchText == null) {
log.error("a null arg: " + this + " " + matchText);
return false;
}
for (var i in this) {
if (matchText == this[i])
return true;
}
return false;
}
return valueList;
}
}
// ==================== PreferencesManager object ====================
function setScreenPosition(node, posIndicator)
{
with (node.style)
{
position = "fixed";
zIndex = 999;
switch (posIndicator) {
case "TL": top = 0; left = 0; break;
case "TR": top = 0; right = 0; break;
case "BL": bottom = 0; left = 0; break;
case "BR": bottom = 0; right = 0; break;
default:
log.error("Unrecognized menu position indicator: " + menuPos);
}
}
}
function PreferencesManager(doc, uniqId, title, buttonDefs)
{
this.doc = extendDocument(doc);
this.uniqId = uniqId;
this.dialogBox = new DialogBox(this.doc, title);
this.buttonDefs = buttonDefs;
/** Display the Preferences dialog.
*/
this.open = function()
{
if (this.doc.selectNodeNullable("//div[@id='" + this.uniqId + "_dialog']")) {
log.info("Preferences dialog already open");
return null; // the dialog is already open
}
var dialogBox_div = this.dialogBox.createDialog(
this.uniqId,
"z-index: 999; left: 15%; top: 25px; position: fixed;"
+ " background-color: LightGray;",
this.buttonDefs
);
with (dialogBox_div.style) {
fontSize = "10pt";
fontFamily = "Arial, Helvetica, sans-serif";
overflow = "auto";
backgroundColor = "LightGray";
}
return dialogBox_div;
}
/** Create an HTML input element associated with the named greasemonkey preference.
*/
this.createPreferenceInput = function(prefName, titleText, tipText, attrMap, optionMap)
{
var prefValue = prefs.get(prefName);
var item_label;
var inputTagname = "input";
switch (typeof(prefValue)) {
case "boolean":
item_label = this.doc.createCheckbox(titleText, attrMap, prefValue);
break;
case "string":
case "number":
if (optionMap != null) {
item_label = this.doc.createSelect(titleText, attrMap, optionMap, prefValue);
inputTagname = "select";
}
else if (attrMap["rows"] != null) {
item_label = this.doc.createTextArea(titleText, attrMap, prefValue);
inputTagname = "textarea";
}
else {
item_label = this.doc.createInputText(titleText, attrMap, prefValue);
}
break;
default:
log.warn("For " + prefName + ", unrecognized type: " + typeof(prefValue));
}
item_label.style.fontSize = "9pt";
if (tipText != null)
item_label.title = tipText;
with (item_label.selectNode(inputTagname)) {
name = prefName;
className = "preferenceSetting";
applyAttributes(attrMap);
}
return item_label;
}
this.createScreenCornerPreference = function(prefName)
{
var prefValue = prefs.get(prefName);
var table = this.doc.createXElement("table", {
id: prefName + "_2x2"
});
with (table) {
style.borderCollapse = "collapse";
cellPadding = 0; cellSpacing = 0;
appendTableRow([ createRadioButton("TL"), null, createRadioButton("TR") ]);
appendTableRow([ null, null, null ]);
appendTableRow([ createRadioButton("BL"), null, createRadioButton("BR") ]);
style.border = "3px inset Black";
foreachNode(".//input", function(inp) {
inp.style.margin = "0px";
});
with (selectNode(".//tr[2]/td[2]")) {
// acheive roughly 4/3 aspect ratio
style.width = "14px";
style.height = "4px";
};
}
return table;
function createRadioButton(choiceValue)
{
var radio_input = doc.createXElement("input", {
type: "radio", name: prefName, value: choiceValue,
className: "preferenceSetting"
});
if (choiceValue == prefValue) {
radio_input.checked = true;
}
return radio_input;
}
}
/** Store current screen values into the associated Preferences,
* but only for values that have changed.
* (This is the primary logic for the OK button)
*/
this.storePrefs = function()
{
this.doc.foreachNode("//*[@class='preferenceSetting']", function(inputObj) {
var prefName = inputObj.name;
var prefValue;
if (inputObj.type == "checkbox") {
prefValue = inputObj.checked;
}
else if (inputObj.type == "radio") {
if (inputObj.checked)
prefValue = inputObj.value;
else
return; // skip all in group except the checked one
}
else {
prefValue = inputObj.value;
}
var oldValue = GM_getValue(prefName, prefValue);
if (prefValue != oldValue)
{
var defaultValue = prefs.get(prefName);
if (typeof(defaultValue) == "number") {
if (isNaN(prefValue)) {
alert("Non-numeric value '" + prefValue + "' is invalid for preference " + prefName);
return false; // continue on to next preference item
}
prefValue = parseFloat(prefValue);
}
if (typeof(prefValue) == "string")
log.info("Setting preference: " + prefName + " => '" + prefValue + "'");
else
log.info("Setting preference: " + prefName + " => " + prefValue);
prefs.set(prefName, prefValue);
}
});
}
}
// ==================== Collapsible object ====================
function Collapsible(theNode, collapserId, isPersistent, isInitExpanded)
{
this.node = theNode;
this.doc = extendDocument(theNode.ownerDocument);
if (collapserId == null) {
if (theNode.id == null)
collapserId = "collapser_" + generateUuid();
else
collapserId = theNode.id + "_collapser";
}
// maintain object reference(s) for callbacks
if (document.activeCollapsers == null) {
document.activeCollapsers = new Object();
}
document.activeCollapsers[collapserId] = this;
this.expand = function(event) {
collapsible = this;
if (event != null) {
var collapserId = event.target.parentNode.id;
collapsible = document.activeCollapsers[collapserId];
if (isPersistent) {
prefs.set(collapserId, true);
}
}
collapsible.node.show();
collapsible.expander.hide();
collapsible.collapser.show();
}
this.collapse = function(event) {
var collapsible = this;
if (event != null) {
var collapserId = event.target.parentNode.id;
collapsible = document.activeCollapsers[collapserId];
if (isPersistent) {
prefs.set(collapserId, false);
}
}
collapsible.node.hide();
collapsible.collapser.hide();
collapsible.expander.show();
}
this.createController = function(func, base64) {
var img = this.doc.createXElement("img");
// img.title = label;
img.src = 'data:image/gif;base64,' + base64;
img.addEventListener('click', func, false);
with (img.style) {
cssFloat = "left";
left = "0px";
position = "absolute";
zIndex = 999;
}
return img;
}
var span = this.doc.createXElement("span", { id: collapserId });
this.node.prependSibling(span);
this.expander = this.createController(this.expand,
'R0lGODlhEAAQAKEDAAAA/wAAAMzMzP///yH5BAEAAAMALAAAAAAQABAAAAIhnI+pywOtwINHTmpvy3rx' +
'nnABlAUCKZkYoGItJZzUTCMFACH+H09wdGltaXplZCBieSBVbGVhZCBTbWFydFNhdmVyIQAAOw=='
);
span.appendChild(this.expander);
this.collapser = this.createController(this.collapse,
'R0lGODlhEAAQAKEDAAAA/wAAAMzMzP///yH5BAEAAAMALAAAAAAQABAAAAIdnI+py+0Popwx0RmEuiAz' +
'6jVS6HTaY5zoyrZuWwAAIf4fT3B0aW1pemVkIGJ5IFVsZWFkIFNtYXJ0U2F2ZXIhAAA7'
);
span.appendChild(this.collapser);
var isExpanded = isInitExpanded;
if (isPersistent == true) {
isExpanded = prefs.get(collapserId);
}
if (isExpanded)
this.expand()
else
this.collapse()
}
// ==================== DocumentContainer object ====================
/** Create and manage invisible iframe content loaded from an arbitrary URL.
* If the same URL is requested more than once, it is returned from cache.
* Example:
* var dc = new DocumentContainer();
* dc.loadFromSameOrigin("search.do?category=eligible",
* function(doc) {
* if (dm.xdoc.selectNode("//text()[.='Dilbert']"))
* alert("Hide your daughters!");
* }
* );
*/
function DocumentContainer(debugFlag)
{
var iframeCache = new Array();
this.debug = debugFlag;
this.loadFromSameOrigin = function(theUrl, theFunc)
{
var iframe = iframeCache[theUrl];
if (iframe != null) {
if (theFunc != null)
theFunc(iframe.contentDocument);
return;
}
var iframe = this.attachIframe(theUrl);
// wait for the DOM to be available, then dispatch
iframe.addEventListener(
"load",
function(evt) {
var theIframe = evt.currentTarget;
var theUrl = theIframe.contentWindow.location.href;
iframeCache[theUrl] = theIframe;
if (theFunc != null)
theFunc(theIframe.contentDocument);
},
false
);
// load the content
iframe.contentWindow.location.href = ajaxstaticUrl(theUrl);
}
this.loadFromForeignOrigin = function(theUrl, theFunc)
{
if (window != top) {
return; // prevent infinite recursion
}
var iframe = this.attachIframe(theUrl);
GM_xmlhttpRequest(
{
method: "GET",
url: ajaxstaticUrl(theUrl),
onload: function(details) {
// give it a URL so that it will create a .contentDocument property.
// Make it the same as the current page,
// Otherwise, same-origin policy would prevent us.
// TBD: why is this a literal? Would foobar.com work as well??
iframe.contentWindow.location.href = "http://tv.yahoo.com/";
// wait for the DOM to be available, then dispatch
iframe.addEventListener(
"DOMContentLoaded",
function() {
if (theFunc != null)
theFunc(iframe.contentDocument);
},
false
);
// write the received content into the document
iframe.contentDocument.open("text/html");
iframe.contentDocument.write(details.responseText);
iframe.contentDocument.close();
}
});
return iframe.contentDocument;
}
this.attachIframe = function(theUrl)
{
// create an IFRAME element to write the document into.
// It must be added to the document and rendered (eg, display != none)
// to be properly initialized.
var iframe = document.createElement("iframe");
iframe.id = "DocumentContainer_" + theUrl;
if (this.debug == null) {
iframe.width = 0;
iframe.height = 0;
iframe.style.display = "none";
}
else {
iframe.width = 800;
iframe.height = 700;
}
document.body.appendChild(iframe);
iframe.contentWindow.location.href = "about:blank";
return iframe;
}
// private helper methods
}
/** Add param to URL, marking it as not to be re-processed.
*/
function ajaxstaticUrl(theUrl)
{
var newUrl = theUrl;
if (newUrl.indexOf("?") == -1)
newUrl += "?";
if (newUrl.indexOf("?") != newUrl.length-1)
newUrl += "&";
return newUrl + "ajaxstatic";
}
/** Retrieve each document specified in the urlList
* invoking onloadFunc with each doc,
* and then finally invoking onrendezvousFunc with the assembled list of docs
*/
function withDocuments(urlList, onloadFunc, onrendezvousFunc)
{
var context = new Object();
context.resultDocList = new Array();
context.pendingCount = urlList.length;
for (var u in urlList)
{
var dc = new DocumentContainer();
dc.loadFromSameOrigin(urlList[u],
function(curDoc) {
if (onloadFunc != null) {
onloadFunc(curDoc);
}
if (--context.pendingCount == 0) {
if (onrendezvousFunc != null) {
context.resultDocList.push(curDoc);
onrendezvousFunc(context.resultDocList);
}
}
}
);
}
}
/** Recursively retrieve each document specified in the urlList,
* then invoke the dispatch function with the list of loaded docs.
*/
function withDocumentsSerialized(urlList, func, docList)
{
var curUrl = urlList.shift();
if (docList == null)
docList = new Array();
var dc = new DocumentContainer();
dc.loadFromSameOrigin(curUrl,
function(curDoc) {
if (urlList.length > 0)
withDocuments(urlList, func, docList);
else
func(docList);
}
);
}
// ==================== Logger object ====================
function Logger(verNum)
{
this.logLevels = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
this.level = null;
this.setLevel = function(level) {
this.level = level;
if (level >= 2)
GM_log("[" + verNum + "] === LOGGER LEVEL: " + this.logLevels[this.level] + " ==="
+ " " + document.location.href);
}
this.setLevel(arrayIndexOf(this.logLevels, prefs.get("loggerLevel")));
this.error = function(msg) { if (this.level >= 0) GM_log("ERROR: " + msg); }
this.warn = function(msg) { if (this.level >= 1) GM_log("WARN: " + msg); }
this.info = function(msg) { if (this.level >= 2) GM_log("INFO: " + msg); }
this.debug = function(msg) { if (this.level >= 3) GM_log("DEBUG: " + msg); }
this.trace = function(msg) { if (this.level >= 4) GM_log("TRACE: " + msg); }
this.getLogLevelMap = function() { return IdentityMapForArray(this.logLevels); };
}
// ==================== JavaScript object extenstions ====================
function extendJavascriptObjects()
{
// ---------- String extensions ----------
/** Format text content as it will appear on a page (before wrapping, etc).
*/
String.prototype.normalizeWhitespace = function()
{
var text = this.replace(/\s+/g, " "); // reduce internal whitespace
text = text.replace(/ ([,;:\.!])/g, "$1"); // snug-up punctuation
return text.trimWhitespace();
}
/** Format text content as it will appear on a page (before wrapping, etc).
*/
String.prototype.trimWhitespace = function()
{
return this.replace(/^\s*/, "").replace(/\s*$/, "");
}
String.prototype.stripQuoteMarks = function()
{
var text = this.replace(/"/g, "");
return text;
}
// ---------- Date extensions ----------
SECOND = 1000;
MINUTE = SECOND * 60;
HOUR = MINUTE * 60;
DAY = HOUR * 24;
WEEK = DAY * 7;
// Example, on the hour: floor(Date.HOUR)
Date.prototype.floor = function(unit) {
var floorMilli = Math.floor(this.getTime() / unit) * unit;
return new Date(floorMilli);
}
Date.prototype.add = function(millis) {
return new Date(this.getTime() + millis);
}
}
// ---------- Array helpers ----------
function arrayIndexOf(theList, value, attrName)
{
if (attrName == null) {
// by element value
for (var i in theList) {
if (theList[i] == value)
return i;
}
}
else {
if (typeof(value) == "object") {
// by corresponding attribute in value array
for (var i in theList) {
if (theList[i][attrName] == value[attrName])
return i;
}
}
else {
// by attribute value
for (var i in theList) {
if (theList[i][attrName] == value) {
return i;
}
}
}
}
return null;
}
function sortBy(theList, fieldList)
{
theList.sort( function(a, b)
{
for (var i in fieldList) {
if (a[fieldList[i]] < b[fieldList[i]]) return -1;
if (a[fieldList[i]] > b[fieldList[i]]) return 1;
}
return 0;
});
return theList;
}
function sortDescBy(theList, fieldList)
{
theList.sort( function(a, b)
{
for (var i in fieldList) {
if (a[fieldList[i]] > b[fieldList[i]]) return -1;
if (a[fieldList[i]] < b[fieldList[i]]) return 1;
}
return 0;
});
return theList;
}
function numericComparatorAsc(a, b) {
return (a-b);
}
function numericComparatorDesc(a, b) {
return (b-a);
}
/** .
*/
function IdentityMapForArray(ary)
{
var map = new Array();
for (var i=0; i < ary.length; i++) {
map[ary[i]] = ary[i];
}
return map;
}
/** Create a new Array with pre-defined numeric indices,
* (ie, ready for inserts to random indices).
*/
function initArrayIndices(count) {
var a = new Array(count);
for (var i = 0; i < count; i++) {
a[i] = null;
}
return a;
}
/** Dispatch processing for each grouping of elements based upon the named field.
* Example:
* var nodes = dm.xdoc.selectNodes("//*[@class]");
* GM_log(nodes.length + " nodes");
* foreachGrouping(sortBy(nodes, ["className"] ), "className", function(groups) {
* GM_log(groups.length + " nodes with class='" + groups[0].className+ "'");
* });
*/
function foreachGrouping(theList, attrName, func)
{
var curList = new Array();
var prevValue = null;
for (var i in theList)
{
if (theList[i][attrName] != prevValue)
{
if (curList.length > 0) {
func(curList);
}
curList = new Array();
}
curList.push(theList[i]);
prevValue = theList[i][attrName];
}
}
// ==================== UrlParser object ====================
/** Parsing and formatting of URLs.
* url, params; scheme, host, port, path
*/
function UrlParser(urlString)
{
var urlParts = urlString.split("?");
this.url = urlParts[0];
this.parms = new Array();
// parse query params into name/value associative list
if (urlParts[1]) {
var queryItems = urlParts[1].split("&");
for (var i in queryItems) {
var parm = queryItems[i].split("=");
this.parms[unescape(parm[0])] = unescape(parm[1]);
// convert to numeric if appropriate
var num = parseInt(parm[1]);
if (!isNaN(num) && parm[1].substring(0, 1) != "0") {
this.parms[unescape(parm[0])] = num;
}
}
}
// parse http://domain/path into scheme, domain, path
this.url.match(/(\w+):\/\/([\w\.]+)(\/.*)/);
this.scheme = RegExp.$1;
this.host = RegExp.$2;
this.path = RegExp.$3;
// METHODS
// assemble the query part of the URL
this.getQuery = function()
{
queryItems = new Array();
for (var p in this.parms) {
if (this.parms[p])
queryItems.push(escape(p) + "=" + escape(this.parms[p]));
}
if (queryItems.length == 0) {
return "";
}
else {
return "?" + queryItems.join("&");
}
}
// assemble the whole URL
this.toString = function()
{
return this.url + this.getQuery();
}
}
// --------------- helper functions ---------------
/** Lookup preference setting and conditionally execute with error handling.
*/
function dispatchFeature(feaureName, func)
{
if (prefs.get(feaureName))
{
tryCatch("feature: " + feaureName, func);
}
}
/** Provide debug info if function throws an exception.
*/
function tryCatch(desc, func)
{
try { func(); }
catch(err) {
log.error(
"exception @ " + err.lineNumber + " [" + desc + "]" + " : " + err + "\n"
+ genStackTrace(arguments.callee)
);
}
}
/** Generate a UUID.
*/
function generateUuid() {
return (S4()+S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4()+S4()+S4());
function S4() {
// 5 digit random #
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}
}
// --------------- Stack Trace ---------------
function genStackTrace(func)
{
var depthLimit = 20;
var stackTrace = "Stack trace:\n";
while (func != null) {
if (--depthLimit < 0) {
stackTrace += "more ...\n";
break;
}
stackTrace += "called by: " + getFunctionSignature(func) + "\n";
// TBD: line# within func
func = func.caller;
}
return stackTrace + "\n\n";
}
function getFunctionSignature(func)
{
var signature = getFunctionName(func);
signature += "(";
for (var i = 0; i < func.arguments.length; i++)
{
// trim long arguments
var nextArgument = func.arguments[i];
if (nextArgument.length > 30)
nextArgument = nextArgument.substring(0, 30) + "...";
// apend the next argument to the signature
signature += "'" + nextArgument + "'";
// comma separator
if (i < func.arguments.length - 1)
signature += ", ";
}
signature += ")";
return signature;
}
function getFunctionName(func)
{
// mozilla makes it easy
if (func.name != null) {
return func.name;
}
// try to parse the function name from the defintion
var definition = func.toString();
var name = definition.substring(
definition.indexOf('function') + 8,
definition.indexOf('(')
);
if (name != null)
return name;
// sometimes there won't be a function name (eg, dynamic functions)
return "anonymous";
}