您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances the content of imdb pages by correlating information from related pages.
// $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"; }