- // $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";
- }