IMDb Weaver

Enhances the content of imdb pages by correlating information from related pages.

  1. // $Id: imdbweaver.user.js 781 2014-06-26 23:26:56Z Chris $
  2. // -----------------------------------------------------------------------------
  3. // This is a Greasemonkey user script.
  4. // To use it, first install Greasemonkey: http://www.greasespot.net/
  5. // Then restart Firefox and revisit this script
  6. // From the Firefox menu select: Tools -> Install User Script
  7. // Accept the default configuration and install
  8. // Now whenever you visit imdb.com you will see extra functionality
  9. // Documentation here: http://refactoror.net/greasemonkey/imdbWeaver/doc.html
  10. // -----------------------------------------------------------------------------
  11.  
  12. // ==UserScript==
  13. // @name IMDb Weaver
  14. // @moniker iwvr
  15. // @namespace http://refactoror.net/
  16. // @description Enhances the content of imdb pages by correlating information from related pages.
  17. // @version 0.3.8.2
  18. // @author Chris Noe
  19. // @include imdb.com/*
  20. // @include *.imdb.com/*
  21. //---------
  22. // @exclude *.imdb.com/images/*
  23. // @exclude *doubleclick*
  24. // @exclude *google_afc*
  25. // @grant GM_getValue
  26. // @grant GM_setValue
  27. // @grant GM_registerMenuCommand
  28. // @grant GM_log
  29. // @grant GM_xmlhttpRequest
  30. // ==/UserScript==
  31.  
  32. var dm = new DomMonkey({
  33. name : "IMDb Weaver"
  34. ,moniker : "iwvr"
  35. ,version : "0.3.8.2"
  36. });
  37.  
  38.  
  39. // The values listed here are the first-time-use defaults
  40. // They have NO EFFECT once this script is installed.
  41. prefs.config({
  42. "ajaxOperationLimit": 10
  43. ,"all-topnav-isExpanded": true
  44. ,"highlightTitleTypes": true
  45. ,"firstMatchAccessKey": "A"
  46. ,"name-headshotMagnifier": true
  47. ,"name-ShowAge": true
  48. ,"name-ShowAgeAtTitleRelease": true
  49. ,"prefsMenuAccessKey": "P"
  50. ,"prefsMenuPosition": "BR"
  51. ,"prefsMenuVisible": true
  52. ,"removeAds": true
  53. ,"title-attributes": true
  54. ,"title-headshotMagnifier": true
  55. ,"title-headshotMagnification": 12
  56. ,"title-ShowAges": true
  57. ,"title-StartResearch": true
  58. });
  59.  
  60. var titleHighlighers = {
  61. Khaki: ["(V)"] // direct to video, no theatrical release
  62. ,Lavender: ["(TV)", "TV series", "TV mini-series", "TV episode"]
  63. ,Pink: ["(VG)"] // video game
  64. };
  65.  
  66.  
  67. // --------------- Page handlers ---------------
  68.  
  69. tryCatch(dm.metadata["moniker"], function () {
  70. if (isStaticPageRequest()) {
  71. // a request from a DocumentContainer object
  72. log.debug("~~~~~ Static page request, no page processing: " + dm.xdoc.location.href);
  73. // TBD: need to add "ajaxstatic" param to all URLs to prevent page graph recursion
  74. dm.xdoc.foreachNode(
  75. "//a[contains(@href, 'imdb.com')]",
  76. function(a) {
  77. a.href = ajaxstaticUrl(a.href);
  78. log.debug("In " + dm.xdoc.location.href + " :: " + a.href);
  79. }
  80. );
  81. return;
  82. }
  83. if (window != window.top) {
  84. log.debug("subordinate window, no page processing: " + document.location.href);
  85. return; // this is a subordinate window (iframe, etc)
  86. }
  87.  
  88. if (dm.xdoc.location.href.match(/\/title\/tt\d+\/?/)) enhanceTitlePage();
  89. else if (dm.xdoc.location.href.match(/\/name\/nm\d+\/?/)) enhanceNamePage();
  90. else if (dm.xdoc.location.href.match(/\/find\?/)) enhanceFindPage();
  91. else if (dm.xdoc.location.href.match(/\/updates\/history/)) enhanceUpdatePage();
  92. else {
  93. log.info("IMDb generic page");
  94. extendImdbDocument(dm.xdoc);
  95. enhanceImdbPage(dm.xdoc);
  96. }
  97. });
  98.  
  99. function isStaticPageRequest() {
  100. return dm.xdoc.location.href.match("ajaxstatic");
  101. }
  102.  
  103.  
  104. var titleDoc;
  105. var nameDoc;
  106.  
  107. // --------- Annoying intervening Ad pages ---------
  108.  
  109. function expediteAdPage()
  110. {
  111. var i = location.href.indexOf("dest=");
  112. var redirUrl = "http:" + location.href.substring(i + 12);
  113. log.info("Ad page, skipping immediately to: " + redirUrl);
  114. // location.href = redirUrl;
  115. }
  116.  
  117. // --------------- Title Page handler ---------------
  118.  
  119. function enhanceTitlePage()
  120. {
  121. log.info("IMDb Title page");
  122. titleDoc = extendImdbTitleDocument(dm.xdoc);
  123. enhanceImdbPage(dm.xdoc);
  124.  
  125. dispatchFeature("removeAds", function()
  126. {
  127. titleDoc.removeAds();
  128. });
  129.  
  130. // Gallery Magnifier
  131. // titleDoc.foreachNode("//div[@class='media_strip_thumbs']//img[contains(@src, '._V1._CR')]", function(img) {
  132. // // substitute the larger image
  133. // img.src = img.src.replace("_V1._CR75,0,300,300_SS90_.jpg", "_V1._SY400_SX600_.jpg");
  134. // img.style.cssFloat = "none";
  135. // var a = img.selectNode("ancestor::a[1]");
  136. // var wrapper_div = a.wrapIn("div", {className: "iwvr_gallery_pic"} );
  137. // wrapper_div.style.minWidth = img.clientWidth;
  138. // });
  139. // titleDoc.addStyle(
  140. // "div.iwvr_gallery_pic:hover a img {\n"
  141. // + " height: auto;\n"
  142. // + " width: auto;\n"
  143. // + " position: absolute;\n"
  144. // + " margin-top: +100px;\n"
  145. // + " margin-left: -25px;\n"
  146. // + "}\n"
  147. // );
  148.  
  149. // createImageMagnifiers(titleDoc, "_V1._CR", /_CR[0-9,_]*/, "_SX600_SY400_");
  150.  
  151. dispatchFeature("title-headshotMagnifier", function()
  152. {
  153. var mag = prefs.get("title-headshotMagnification") || 12;
  154. var pixels = mag * 32;
  155. var fileSfx = "_V1._SX" + pixels + "_SY" + pixels + "_";
  156. createImageMagnifiers(titleDoc, "_V1._SX23_SY30_", /_SX\d+_SY\d+_/, fileSfx);
  157. });
  158.  
  159. dispatchFeature("title-attributes", function()
  160. {
  161. addTitleAttrs();
  162. });
  163.  
  164. // Display rating/runtime/language directly below title
  165. function addTitleAttrs()
  166. {
  167. var titleAttrs = new Array();
  168.  
  169. var cert = titleDoc.getCertification();
  170. if (cert != null) {
  171. titleAttrs.push(cert);
  172. }
  173. var runtime = titleDoc.getRuntime();
  174. if (runtime != null) {
  175. var rt = runtime + " min";
  176. if (runtime > 60) {
  177. rt += " (" + formatHoursMinutes(runtime) + ")";
  178. }
  179. titleAttrs.push(rt);
  180. }
  181. var lang = titleDoc.getLanguage();
  182. if (lang != null) {
  183. titleAttrs.push(lang);
  184. }
  185.  
  186. var languages = [];
  187. titleDoc.foreachNode("//a[contains(@href,'/language/')]", function(lang_a) {
  188. languages.push(lang_a.textContent);
  189. });
  190. titleAttrs.push(languages.join("/"));
  191.  
  192. var title_div = titleDoc.selectNodeNullable("//div[@id='tn15title']");
  193. if (titleAttrs.length > 0) {
  194. title_div.appendChildText(titleAttrs.join(", "));
  195. }
  196. }
  197.  
  198. function formatHoursMinutes(min)
  199. {
  200. var m = "0" + min % 60; // (extra leading zero)
  201. var h = (min - m) / 60;
  202. return h + ":" + m.substring(m.length - 2);
  203. }
  204.  
  205. addCastToolbar();
  206.  
  207. function addCastToolbar()
  208. {
  209. var toolbar_span = titleDoc.createXElement("span", {id: "iwvr_casttoolbar"} );
  210. var quotedTitle = '"' + titleDoc.getTitle() + '"';
  211.  
  212. dispatchFeature("title-ShowAges", function()
  213. {
  214. var showCastAges_button = titleDoc.createXElement("button", {id: "iwvr_showCastAges"} );
  215. with (showCastAges_button) {
  216. className = "iwvr_button";
  217. textContent = "Show Ages";
  218. title = "Show cast ages when " + quotedTitle + " was released in " + titleDoc.getTitleYear();
  219. addEventListener('click', showCastAges, false);
  220. }
  221. with (toolbar_span) {
  222. appendChildTextNbsp(3);
  223. appendChild(showCastAges_button);
  224. }
  225. });
  226.  
  227. dispatchFeature("title-StartResearch", function()
  228. {
  229. var startResearch_button = titleDoc.createXElement("button", {id: "iwvr_startReasearch"} );
  230. with (startResearch_button) {
  231. className = "iwvr_button";
  232. addEventListener('click', startResearch, false);
  233. textContent = "Start Research...";
  234. title = "Mine additional information about " + quotedTitle;
  235. }
  236. with (toolbar_span) {
  237. appendChildTextNbsp(3);
  238. appendChild(startResearch_button);
  239. }
  240. });
  241.  
  242. var cast_table = titleDoc.getCast_table();
  243. if (cast_table != null) {
  244. cast_table.prependSibling(toolbar_span);
  245. }
  246. }
  247.  
  248. function showCastAges(event)
  249. {
  250. // prevent second execution on same page
  251. event.target.removeEventListener('click', showCastAges, false);
  252.  
  253. // process each cast member name
  254. var nodeCount = 0;
  255. var ajaxOperationLimit = prefs.get("ajaxOperationLimit");
  256. titleDoc.foreachCastMember_tr
  257. (
  258. "//td[@class='nm']"
  259. + titleDoc.CAST_NAMES_A
  260. + "[not(following-sibling::span[@id='iwvr_age'])]",
  261. function(a_name)
  262. {
  263. if (nodeCount < ajaxOperationLimit || ajaxOperationLimit == -1)
  264. {
  265. var titleDC = new DocumentContainer();
  266. log.debug("%%%%% loadFromSameOrigin: " + a_name.href);
  267. titleDC.loadFromSameOrigin(
  268. // extract info from the cast member page
  269. a_name.href,
  270. function(doc) {
  271. var nameDoc = extendImdbNameDocument(doc);
  272. var titleYear = titleDoc.getTitleYear();
  273. if (titleYear == null || titleYear == "") {
  274. age = "?";
  275. }
  276. else {
  277. var age = nameDoc.getAge(titleYear);
  278. if (age == null)
  279. age = "?";
  280. }
  281. // var dd_a = nameDoc.selectNodeNullable("//a[contains(@href, 'death_date=')]");
  282. // if (dd_a)
  283. // dd = " " + dd_a.textContent;
  284. var age_span = titleDoc.createXElement("span", {id: "iwvr_age"} );
  285. age_span.appendChildText(" (" + age + ")");
  286. a_name.appendSibling(age_span);
  287. }
  288. );
  289. }
  290. nodeCount++;
  291. }
  292. )
  293. event.target.textContent = "Show More Ages";
  294. event.target.addEventListener('click', showCastAges, false);
  295. }
  296.  
  297. function startResearch()
  298. {
  299. if (titleDoc.selectNodeNullable("//div[@id='iwvr_researchItems_dialog']")) {
  300. return; // the dialog is already open
  301. }
  302.  
  303. // make all names on page draggable
  304. titleDoc.foreachNode(
  305. titleDoc.CAST_NAMES_A,
  306. function(name_a) {
  307. name_a.className = "iwvr_researchable_item";
  308. name_a.addEventListener('draggesture', addResearchItem, false);
  309. }
  310. );
  311.  
  312. function addResearchItem(event)
  313. {
  314. var original_item_a = event.target.parentNode;
  315. var dragged_item_a = original_item_a.cloneNode(true);
  316.  
  317. var tip = titleDoc.selectNodeNullable("//*[@id='draggingTip']");
  318. if (tip != null)
  319. tip.remove();
  320.  
  321. var ul = titleDoc.selectNode("//ul[@id='iwvr_research_itemlist']");
  322. var li = titleDoc.createXElement("li");
  323. li.appendChild(dragged_item_a);
  324. ul.appendChild(li);
  325.  
  326. original_item_a.className = null;
  327. original_item_a.removeEventListener('draggesture', addResearchItem, false);
  328. };
  329.  
  330. // present the Research dialog
  331. var researchDialog = new DialogBox(titleDoc, "Research");
  332. var researchDialog_div = researchDialog.createDialog(
  333. "iwvr_researchItems",
  334. "z-index: 999; position: fixed; bottom: 5px; right: 10px;",
  335. { OK: okResearch, Cancel: cancelResearch }
  336. );
  337. researchDialog.main_td.style.padding = "6px";
  338. with (researchDialog_div)
  339. {
  340. style.fontSize = "10pt";
  341. style.fontFamily = "Arial, Helvetica, sans-serif";
  342.  
  343. researchDialog_div.style.overflow = "auto";
  344.  
  345. var sel = titleDoc.createSelect(
  346. "Research",
  347. { name: "researchmode" },
  348. { tic: "Titles in common" }, "tic");
  349. appendChild(sel);
  350.  
  351. var scroll_div = titleDoc.createXElement("div");
  352. with (scroll_div) {
  353. with (style) {
  354. border = "1px inset Black"; margin = 4; padding = 5;
  355. width = 200; height = 60; overflow = "auto";
  356. }
  357.  
  358. var items_ul = titleDoc.createXElement("ul", {id: "iwvr_research_itemlist"} );
  359. with (items_ul.style) {
  360. marginTop = "0px"; paddingTop = "0px";
  361. marginLeft = "0px"; paddingLeft = "1em";
  362. }
  363. appendChild(items_ul);
  364.  
  365. var tip_div = titleDoc.createXElement("div");
  366. tip_div.id = "draggingTip";
  367. with (tip_div.style) { color = "Gray";
  368. width = "100%"; textAlign = "center";
  369. height = "85%"; verticalAlign = "middle";
  370. }
  371. tip_div.appendChildText("Drag Names Here");
  372. appendChild(tip_div);
  373. }
  374. appendChild(scroll_div);
  375.  
  376. var includes_div = titleDoc.createTopicDiv("Include");
  377. includes_div.style.borderColor = "DarkGreen";
  378. // includes_div.style.backgroundColor = "#66CC33";
  379. with (includes_div.contentElement)
  380. {
  381. appendChild(titleDoc.createCheckbox(
  382. "Individual Episodes",
  383. {id: "include-episodes"},
  384. false
  385. ));
  386. }
  387. appendChild(includes_div);
  388.  
  389. var excludes_div = titleDoc.createTopicDiv("Exclude Categories");
  390. excludes_div.style.borderColor = "#AA0000";
  391. // excludes_div.style.backgroundColor = "#FF3300";
  392. with (excludes_div.contentElement)
  393. {
  394. appendChild(titleDoc.createCheckbox(
  395. "Self",
  396. {id: "exclude-self", title: "Examples: Oprah, Letterman"},
  397. true
  398. ));
  399. appendChildElement("br");
  400. appendChild(titleDoc.createCheckbox(
  401. "Archive Footage",
  402. {id: "exclude-archive-footage"},
  403. true
  404. ));
  405. }
  406. appendChild(excludes_div);
  407. }
  408. }
  409.  
  410. function okResearch(doc)
  411. {
  412. var titleDoc = extendImdbTitleDocument(doc);
  413.  
  414. // get URLs of selected research items
  415. var peopleUrls = new Array();
  416. titleDoc.foreachNode("//ul[@id='iwvr_research_itemlist']//a", function(name_a) {
  417. peopleUrls.push(name_a.href);
  418. });
  419.  
  420. var includeEpisodes = titleDoc.selectNode("//input[@id='include-episodes']").checked;
  421.  
  422. var omitCategories = new Array();
  423. if (titleDoc.selectNode("//input[@id='exclude-self']").checked)
  424. omitCategories.push("self");
  425. if (titleDoc.selectNode("//input[@id='exclude-archive-footage']").checked)
  426. omitCategories.push("archive");
  427.  
  428. endResearch(titleDoc);
  429. if (peopleUrls.length > 0) {
  430. correlatePeople(peopleUrls, includeEpisodes, omitCategories);
  431. }
  432. }
  433.  
  434. function cancelResearch(doc)
  435. {
  436. var titleDoc = extendImdbTitleDocument(doc);
  437. endResearch(titleDoc);
  438. }
  439.  
  440. function endResearch(titleDoc)
  441. {
  442. // remove class="iwvr_researchable_item"
  443. titleDoc.foreachNode(
  444. titleDoc.CAST_NAMES_A,
  445. function(name_a) {
  446. name_a.className = null;
  447. }
  448. );
  449. }
  450.  
  451. var DEFAULT_JOB_ABBREVS = {
  452. "A": "Actor",
  453. "A'": "Actress",
  454. "D": "Director",
  455. "P": "Producer",
  456. "PD": "Production Designer",
  457. "W": "Writer",
  458. "C": "Composer",
  459. "ST": "Soundtrack",
  460. "AD": "Art Department",
  461. "MC": "Miscellaneous Crew",
  462. "S": "Self",
  463. "Ar": "Archive Footage"
  464. };
  465.  
  466. // gather credits for specified people and
  467. // determine what titles they have in common
  468. function correlatePeople(urlList, includeEpisodes, omitCatList)
  469. {
  470. // foreach (var User in Users)
  471. // {
  472. // <div class="action-time">[ActionSpan(User)]</div>
  473. // if (User.IsAnonymous)
  474. // {
  475. // <div class="gravatar32">[RenderGravatar(User)]</div>
  476. // <div class="details">[UserRepSpan(User)]<br/>[UserFlairSpan(User)]</div>
  477. // }
  478. // else
  479. // {
  480. // <div class="anon">anonymous</div>
  481. // }
  482. // }
  483. var jobAbbrevs = new AbbreviationMap(DEFAULT_JOB_ABBREVS);
  484.  
  485. var jointCredits_a = new Array();
  486. var nameSet_a = new Array();
  487.  
  488. withDocuments(
  489. urlList, // gather credits from each person's page
  490. function(doc) {
  491. var nameDoc = extendImdbNameDocument(doc);
  492. var name_a = nameDoc.getName_a();
  493. nameSet_a.push(name_a);
  494. jointCredits_a = jointCredits_a.concat(
  495. nameDoc.getCredits_a( {imdbName_a: name_a}, includeEpisodes, omitCatList)
  496. );
  497. },
  498. function(docList)
  499. {
  500. // sort combined credits (by url)
  501. sortBy(jointCredits_a, ["href"] );
  502. sortBy(nameSet_a, ["textContent"] );
  503.  
  504. var creditsInCommon_a = new Array();
  505. var soloCreditCount = 0;
  506. foreachGrouping(jointCredits_a, "href", function(creds_a)
  507. {
  508. if (creds_a.length < 2) {
  509. soloCreditCount++;
  510. return; // exclude where not in common with anybody else
  511. }
  512.  
  513. var nameJobMap = new Array();
  514. for (var c in creds_a) {
  515. var jobList = jobAbbrevs.registerList(creds_a[c].categoryList);
  516. nameJobMap[creds_a[c].propertySet.imdbName_a] = jobList;
  517. }
  518.  
  519. var combo_a = creds_a[0].cloneNode(true);
  520. combo_a.nameJobMap = nameJobMap;
  521.  
  522. creditsInCommon_a.push(combo_a);
  523. });
  524.  
  525. // sort combined credits (by title)
  526. sortBy(creditsInCommon_a, ["textContent"] );
  527.  
  528. // create information popup
  529. var resultsWindow = new DialogBox(titleDoc, "Research Results");
  530. var resultsWindow_div = resultsWindow.createDialog(
  531. "iwvr_creditsInCommon",
  532. "z-index: 999; position: fixed; left: 15px; top: 20px;",
  533. { X: noop }
  534. );
  535. resultsWindow.main_td.style.padding = "6px";
  536. with (resultsWindow_div)
  537. {
  538. style.maxWidth = window.innerWidth - 70;
  539. style.maxHeight = window.innerHeight - 90;
  540. // style.overflow = "auto";
  541.  
  542. // construct the data table
  543. var i = 1;
  544. var table = titleDoc.createXElement("table", {className: "iwvr_results"} );
  545. var caption = titleDoc.createXElement("caption");
  546. caption.appendChildText("Credits in common", ["b"]);
  547. table.appendChild(caption);
  548. var cellAttrList = [
  549. {className: "iwvr_results bulletcol"},
  550. {className: "iwvr_results titlecol"}
  551. ].concat(
  552. dup(nameSet_a.length, {className: "iwvr_results datacol"} )
  553. );
  554. var thead = titleDoc.createXElement("thead");
  555. thead.appendTableRow([null, "Title"].concat(nameSet_a), cellAttrList);
  556. table.appendChild(thead);
  557. var tbody = titleDoc.createXElement("tbody");
  558. for (var c in creditsInCommon_a)
  559. {
  560. var rowSet = initArrayIndices(2 + nameSet_a.length);
  561. rowSet[0] = i + ")";
  562. var credit_span = titleDoc.createXElement("span");
  563. if (creditsInCommon_a[c].seriesTitle != null) {
  564. credit_span.appendChildText(creditsInCommon_a[c].seriesTitle);
  565. }
  566. credit_span.appendChild(creditsInCommon_a[c]);
  567. rowSet[1] = credit_span;
  568. for (var name_a in creditsInCommon_a[c].nameJobMap) {
  569. var jobList = creditsInCommon_a[c].nameJobMap[name_a];
  570. var col = 2 + parseInt(arrayIndexOf(nameSet_a, name_a));
  571. rowSet[col] = jobList.sort().join(", ");
  572. }
  573. tbody.appendTableRow(rowSet, cellAttrList);
  574. i++;
  575. }
  576. var legend_div = jobAbbrevs.toLegend("div",
  577. { className: "iwvr_results_legend" } );
  578. tbody.appendTableRow(
  579. [ legend_div ],
  580. [ { colSpan: (2 + nameSet_a.length) } ]
  581. );
  582. table.appendChild(tbody);
  583.  
  584. appendChild(table);
  585. }
  586. }
  587. );
  588. }
  589. }
  590.  
  591.  
  592. function dup(count, value) {
  593. var returnValue = new Array();
  594. for (var i = 0; i < count; i++) {
  595. returnValue.push(value);
  596. }
  597. return returnValue;
  598. }
  599.  
  600. // --------------- Name Page handler ---------------
  601.  
  602. function enhanceNamePage()
  603. {
  604. log.info("IMDb Name page");
  605. var nameDoc = extendImdbNameDocument(dm.xdoc);
  606. enhanceImdbPage(dm.xdoc);
  607.  
  608. dispatchFeature("removeAds", function()
  609. {
  610. nameDoc.removeAds();
  611. });
  612.  
  613. dispatchFeature("name-headshotMagnifier", function()
  614. {
  615. createImageMagnifiers(nameDoc, "_V1._SX32_", /_SX\d+_/, "_SX640_SY720_");
  616. });
  617.  
  618. var birthInfo_div = nameDoc.selectNodeNullable(
  619. "//a[contains(@href, 'birth_year=')]/ancestor::div[1]");
  620. var age = nameDoc.getAge();
  621.  
  622. var deathInfo_div = nameDoc.selectNodeNullable(
  623. "//a[contains(@href, 'death_date=')]/ancestor::div[1]");
  624. var ageDeath = nameDoc.getAgeDeath();
  625.  
  626. dispatchFeature("highlightTitleTypes", function()
  627. {
  628. // first defeat the site's regular even/odd row highlighting
  629. nameDoc.foreachNode("//tbody[contains(@class, 'row-filmo-')]", function(tbody)
  630. {
  631. tbody.style.backgroundColor = "White";
  632. });
  633. // now add custom highlighting
  634. highlightTitleTypes(titleHighlighers);
  635. function highlightTitleTypes(hSpecs)
  636. {
  637. for (var color in hSpecs) {
  638. var matchStrs = hSpecs[color];
  639. for (var i in matchStrs) {
  640. nameDoc.foreachNode([
  641. "//text()[contains(., '" + matchStrs[i] + "')]//ancestor-or-self::tbody[1]",
  642. "//text()[contains(., '" + matchStrs[i] + "')]//ancestor-or-self::li[1]"
  643. ],
  644. function(item)
  645. {
  646. item.style.backgroundColor = color;
  647. });
  648. }
  649. }
  650. }
  651. });
  652.  
  653. dispatchFeature("name-ShowAge", function()
  654. {
  655. if (birthInfo_div == null || age == null) {
  656. log.info("name-ShowAge: no birth year");
  657. }
  658. else {
  659. birthInfo_div.appendChildElement("br");
  660. birthInfo_div.appendChildText(
  661. ((nameDoc.getDeathDetails_div() != null) ? "would be " : "is ")
  662. + age + " years old"
  663. );
  664. }
  665.  
  666. if (deathInfo_div == null || ageDeath == null) {
  667. log.info("name-ShowAge: no death year");
  668. }
  669. else {
  670. deathInfo_div.appendChildElement("br");
  671. deathInfo_div.appendChildText(
  672. "at age " + ageDeath
  673. );
  674. }
  675. });
  676.  
  677. dispatchFeature("name-ShowAgeAtTitleRelease", function()
  678. {
  679. // if we are arriving at a person's page from a specific title page
  680. if (dm.xdoc.referrer.match("imdb.com/title"))
  681. {
  682. // trim all but base title URL, (could be arriving from other detail pages)
  683. dm.xdoc.referrer.match(/(.*tt\d*)/);
  684. var referrerUrl = RegExp.$1;
  685.  
  686. var nameDC = new DocumentContainer();
  687. nameDC.loadFromSameOrigin(
  688. // use info from the referring title page
  689. referrerUrl,
  690. function(doc)
  691. {
  692. var titleDoc = extendImdbTitleDocument(doc);
  693. var titleYear = titleDoc.getTitleYear();
  694. var ageThen;
  695. if (titleYear == null || titleYear == "") {
  696. ageThen = "?";
  697. }
  698. else {
  699. ageThen = nameDoc.getAge(titleYear);
  700. if (ageThen == null)
  701. ageThen = "?";
  702. }
  703.  
  704. if (birthInfo_div == null || age == null) {
  705. log.info("name-ShowAgeAtTitleRelease: no birth year");
  706. return;
  707. }
  708.  
  709. birthInfo_div.appendChildElement("br");
  710. var t;
  711. if (titleYear > (new Date()).getFullYear())
  712. t = '(will be ' + ageThen
  713. + ' when "' + titleDoc.getTitle()
  714. + '" releases in ' + titleYear + ')'
  715. ;
  716. else
  717. t = '(was ' + ageThen
  718. + ' when "' + titleDoc.getTitle()
  719. + '" was released in ' + titleYear + ')'
  720. ;
  721. birthInfo_div.appendChildText(t);
  722. }
  723. );
  724. }
  725. else {
  726. if (dm.xdoc.referrer == "") {
  727. log.warn("The name-ShowAgeAtTitleRelease option is enable"
  728. + " , but document.referrer is empty. REFERRER MAY BE DISABLED."
  729. );
  730. }
  731. }
  732. });
  733. }
  734.  
  735. // --------------- Find Page handler ---------------
  736.  
  737. function enhanceFindPage()
  738. {
  739. log.info("IMDb Find page");
  740. var findDoc = extendImdbNameDocument(dm.xdoc);
  741. enhanceImdbPage(dm.xdoc);
  742.  
  743. // assign access key to first matching item
  744. var accessKey = prefs.get("firstMatchAccessKey");
  745. if (accessKey != null)
  746. {
  747. // first text link to "/rg/find-", that is inside a TD
  748. var firstMatch_a = findDoc.selectNodeNullable(
  749. "//td/a[not(img)][contains(@onclick, '/rg/find-')][1]");
  750. if (firstMatch_a != null) {
  751. firstMatch_a.accessKey = accessKey.toUpperCase();
  752. var itemNum_td = firstMatch_a.selectNodeNullable("preceding::td[1]");
  753. if (itemNum_td != null) {
  754. var span = findDoc.createXElement("span");
  755. span.appendChildText("[" + accessKey + "]");
  756. firstMatch_a.href.match(/\/(\w+)\//);
  757. var linkType = RegExp.$1; // "title" or "name"
  758. span.title = "Alt-Shift-" + accessKey + " to go to this " + linkType;
  759. span.style.fontWeight = "bold";
  760. itemNum_td.innerHTML = "";
  761. itemNum_td.appendChild(span);
  762. }
  763. }
  764. }
  765.  
  766. dispatchFeature("highlightTitleTypes", function()
  767. {
  768. highlightTitleTypes(titleHighlighers);
  769. function highlightTitleTypes(hSpecs)
  770. {
  771. for (var color in hSpecs) {
  772. var matchStrs = hSpecs[color];
  773. for (var i in matchStrs) {
  774. findDoc.foreachNode("//a[contains(@onclick, '/rg/find-title')]/ancestor::table[1]//text()[contains(., '" + matchStrs[i] + "')]"
  775. + "//ancestor-or-self::td[1]", function(item)
  776. {
  777. item.style.backgroundColor = color;
  778. });
  779. }
  780. }
  781. }
  782. });
  783.  
  784. // // Poster Magnifier
  785. // findDoc.foreachNode("//a[contains(@href, 'title-tiny')]/img", function(img) {
  786. // // substitute the larger image
  787. // img.src = img.src.replace(/(\d)t\.(jpg|png|gif)$/, "$1m.$2");
  788. // img.className = "iwvr_headshot";
  789. // img.height = null;
  790. // img.width = null;
  791. // });
  792. // titleDoc.addStyle(
  793. // "img.iwvr_headshot { height: 32px; width: 22px; }\n"
  794. // + "td:hover img.iwvr_headshot {\n"
  795. // + " height: auto;\n"
  796. // + " width: auto;\n"
  797. // + " position: absolute;\n"
  798. // + " margin-top: -59px;\n"
  799. // + " margin-left: -125px;\n"
  800. // + "}\n"
  801. // );
  802. }
  803.  
  804. // --------------- Find Page handler ---------------
  805.  
  806. function enhanceUpdatePage()
  807. {
  808. log.info("IMDb updates page");
  809.  
  810. // assign access key to first matching item
  811. var accessKey = prefs.get("firstMatchAccessKey");
  812. if (accessKey)
  813. {
  814. var updateDoc = extendDocument(dm.xdoc);
  815. // first update item
  816. var firstMatch_a = updateDoc.selectNodeNullable("(//a[contains(@href, 'update?load=')])[1]");
  817. if (firstMatch_a) {
  818. firstMatch_a.accessKey = accessKey.toUpperCase();
  819. var item_td = firstMatch_a.selectNodeNullable("ancestor::td[1]");
  820. if (item_td) {
  821. var span = updateDoc.createXElement("span");
  822. span.appendChildText("[" + accessKey + "]");
  823. span.style.fontWeight = "bold";
  824. item_td.prependChild(span);
  825. }
  826. }
  827. }
  828. }
  829.  
  830. // --------------- Find Page handler ---------------
  831.  
  832. function enhanceImdbPage(imdbDoc)
  833. {
  834. log.info("enhanceImdbPage");
  835. imdbDoc.removeAds();
  836.  
  837. var navbar_div = imdbDoc.selectNodeNullable("//div[@id='nb20']");
  838. if (navbar_div != null) {
  839. navbar_div.makeCollapsible("all-topnav-isExpanded", true);
  840. }
  841. }
  842.  
  843.  
  844. // --------------- Title Page extensions ---------------
  845.  
  846. function extendImdbTitleDocument(titleDoc)
  847. {
  848. if (titleDoc == null)
  849. return null;
  850.  
  851. extendImdbDocument(titleDoc);
  852.  
  853. titleDoc.removeAds_super = titleDoc.removeAds;
  854. titleDoc.removeAds = function()
  855. {
  856. this.removeAds_super();
  857.  
  858. titleDoc.foreachNode([
  859. "//div[@id='tn15shopbox']"
  860. ,"//div[@id='tn15adrhs']"
  861. ,"//div[starts-with(@id, 'banner')]"
  862. ,"//div[@id='tn15tc']"
  863. // ,"//a[contains(href, NAVSTRIP)]"
  864. ], function(node) {
  865. node.remove();
  866. log.debug("Removing ad element: <" + node.tagName + " id=" + node.id);
  867. }
  868. );
  869. }
  870.  
  871. titleDoc.getTitle = function()
  872. {
  873. var title;
  874. var poster_a = this.selectNodeNullable("//a[@name='poster']");
  875. if (poster_a != null) {
  876. title = poster_a.title;
  877. }
  878. else {
  879. var title_span = this.selectNode("//title/text()");
  880. title = title_span.textContent.replace(/\(\d\d\d\d\)/, "");
  881. }
  882. return title.stripQuoteMarks().normalizeWhitespace();
  883. };
  884.  
  885. titleDoc.getTitle_a = function()
  886. {
  887. var a = this.createXElement("a");
  888. a.href = this.location.href;
  889. a.appendChildText(this.getTitle());
  890. return a;
  891. }
  892.  
  893. titleDoc.getTitleYear = function() {
  894. var year;
  895.  
  896. var airDate = this.selectTextContent(
  897. "//*[text()='Original Air Date:']/following-sibling::*[1]/text()");
  898. if (airDate != null) {
  899. airDate.match(/\d+ \w+ (\d\d\d\d)/);
  900. year = RegExp.$1;
  901. return year;
  902. }
  903.  
  904. var year = this.selectTextContent(
  905. "//div[@id='tn15title']//a[contains(@href, '/year/')]/text()");
  906.  
  907. return year;
  908. };
  909.  
  910. titleDoc.getUserRating = function() {
  911. var userRating = this.selectTextContent(
  912. "//*[contains(text(), 'User Rating:')]/following-sibling::*[1]");
  913. if (userRating == null)
  914. return null;
  915. userRating = userRating.split("/");
  916. return userRating[0] / userRating[1];
  917. };
  918.  
  919. titleDoc.getRuntime = function() {
  920. var runtime_text = this.selectNodeNullable(
  921. "//*[text()='Runtime:']/following-sibling::*[1]/text()");
  922. if (runtime_text == null) {
  923. return null;
  924. }
  925. var runtime = runtime_text.textContent.match(/(\d+)/);
  926. return RegExp.$1;
  927. };
  928.  
  929. titleDoc.getCertification = function(country) {
  930. if (country == null) {
  931. country = "USA";
  932. }
  933. var cert = this.selectTextContent(
  934. "//a[starts-with(@href, concat('/List?certificates=', '" + country + ":'))]");
  935. return cert;
  936. };
  937.  
  938. titleDoc.getLanguage = function() {
  939. var lang_a = this.selectNodeNullable(
  940. "//a[starts-with(@href, '/Sections/Languages/')]");
  941. if (lang_a == null) {
  942. return null;
  943. }
  944. return lang_a.textContent;
  945. };
  946.  
  947. titleDoc.CAST_TABLE =
  948. "//table[@class='cast']"
  949. ;
  950. titleDoc.CAST_NAMES_A = "//a[starts-with(@href, '/name/')]";
  951. titleDoc.CHARACTER_NAME_A = "//a[@href='quotes']";
  952.  
  953. titleDoc.foreachCastMember_tr = function(relativeXpath, func)
  954. {
  955. this.foreachNode(
  956. titleDoc.CAST_TABLE
  957. + "//tr"
  958. + relativeXpath,
  959. func
  960. );
  961. }
  962.  
  963. titleDoc.getCast_table = function() {
  964. return this.selectNodeNullable(titleDoc.CAST_TABLE);
  965. };
  966.  
  967. log.debug(
  968. "extendImdbTitleDocument: "
  969. + "title='" + titleDoc.getTitle() + "'"
  970. + ", titleYear=" + titleDoc.getTitleYear()
  971. + ", userRating=" + titleDoc.getUserRating()
  972. );
  973.  
  974. return titleDoc;
  975. }
  976.  
  977.  
  978. // --------------- Title Page extensions ---------------
  979.  
  980. function extendImdbNameDocument(nameDoc)
  981. {
  982. if (nameDoc == null)
  983. return null;
  984.  
  985. extendImdbDocument(nameDoc);
  986.  
  987. nameDoc.removeAds_super = nameDoc.removeAds;
  988. nameDoc.removeAds = function()
  989. {
  990. this.removeAds_super();
  991.  
  992. var ad_div = nameDoc.selectNodeNullable("//div[@id='tn15adrhs']");
  993. if (ad_div != null) {
  994. ad_div.remove();
  995. }
  996. }
  997.  
  998. nameDoc.getName = function() {
  999. return this.selectTextContent("//title");
  1000. };
  1001.  
  1002. nameDoc.getName_a = function()
  1003. {
  1004. var a = this.createXElement("a");
  1005. a.href = this.location.href;
  1006. a.appendChildText(this.getName());
  1007. return a;
  1008. }
  1009.  
  1010. nameDoc.getAge = function(refYear) {
  1011. if (refYear == null) {
  1012. refYear = (new Date()).getFullYear();
  1013. }
  1014. var birthYear = this.getBirthYear();
  1015. if (birthYear == null) {
  1016. return null;
  1017. }
  1018. var age = refYear - birthYear;
  1019. if (isNaN(age)) {
  1020. return "??";
  1021. }
  1022. else {
  1023. return age;
  1024. }
  1025. };
  1026.  
  1027. nameDoc.getBirthYear = function() {
  1028. var birthYear_a = this.selectNodeNullable(
  1029. "//a[contains(@href, 'birth_year=')]"
  1030. );
  1031. if (birthYear_a == null)
  1032. return null;
  1033. else
  1034. return birthYear_a.textContent;
  1035. };
  1036.  
  1037. nameDoc.getBirthDetails_end = function() {
  1038. var birthDetails_end = this.selectNodeNullable(
  1039. "//a[contains(@href, 'birth_year=')]"
  1040. + "/following::br[1]"
  1041. );
  1042. return birthDetails_end;
  1043. }
  1044.  
  1045. nameDoc.getAgeDeath = function() {
  1046. var deathYear = this.getDeathYear();
  1047. if (deathYear == null) {
  1048. return null;
  1049. }
  1050. var ageDeath = deathYear - this.getBirthYear();
  1051. if (isNaN(ageDeath)) {
  1052. return "??";
  1053. }
  1054. else {
  1055. return ageDeath;
  1056. }
  1057. };
  1058.  
  1059. nameDoc.getDeathYear = function() {
  1060. var deathYear_a = this.selectNodeNullable(
  1061. "//a[contains(@href, 'death_date=')]"
  1062. );
  1063. if (deathYear_a == null)
  1064. return null;
  1065. else
  1066. return deathYear_a.textContent;
  1067. };
  1068.  
  1069. nameDoc.getDeathDetails_div = function() {
  1070. return this.selectNodeNullable(
  1071. "//*[contains(@href, 'death_date=')]"
  1072. + "//ancestor::div[1]"
  1073. );
  1074. }
  1075.  
  1076. nameDoc.CREDIT_CATEGORIES_A =
  1077. "//a[starts-with(@href, '/title/')]"
  1078. + "/ancestor::div[@class='filmo']/descendant::a[@name][1]";
  1079.  
  1080. // Get all the credits on this page, grouped by title.
  1081. // The list of job categories is attached to each "merged" title reference.
  1082. nameDoc.getCredits_a = function(propSet, includeEpisodes, omitCatList)
  1083. {
  1084. var creditList_a = new Array();
  1085. nameDoc.foreachNode( // Director, Writer, Actor, etc
  1086. nameDoc.CREDIT_CATEGORIES_A,
  1087. function (category_a)
  1088. {
  1089. var catLabel = category_a.textContent.replace(/:/, "");
  1090. if (arrayIndexOf(omitCatList, category_a.name))
  1091. return;
  1092.  
  1093. var matchCredits = "//a[@name='" + category_a.name + "']"
  1094. + "/following::ol[1]/li/a[1]";
  1095.  
  1096. nameDoc.foreachNode( // each credit within a category
  1097. matchCredits,
  1098. function (credit_a) {
  1099. credit_a.category = catLabel;
  1100. if (propSet != null) {
  1101. credit_a.propertySet = propSet;
  1102. }
  1103. creditList_a.push(credit_a);
  1104.  
  1105. if (includeEpisodes) {
  1106. credit_a.foreachNode( // each sub-credit within a credit
  1107. "following-sibling::a",
  1108. function (subCredit_a) {
  1109. subCredit_a.category = catLabel;
  1110. if (propSet != null) {
  1111. subCredit_a.propertySet = propSet;
  1112. }
  1113. subCredit_a.seriesTitle = credit_a.textContent;
  1114. creditList_a.push(subCredit_a);
  1115. }
  1116. );
  1117. }
  1118. });
  1119. });
  1120.  
  1121. return mergeCreditCategories(creditList_a);
  1122.  
  1123. // for each title combine multiple credits into a single object
  1124. // with an array property that lists the category names
  1125. function mergeCreditCategories(creditList_a)
  1126. {
  1127. sortBy(creditList_a, ["href"] );
  1128.  
  1129. var mergedCredits = new Array();
  1130. foreachGrouping(creditList_a, "href", function(creds_a)
  1131. {
  1132. var catList = new Array();
  1133. for (var i in creds_a) {
  1134. catList.push(creds_a[i].category);
  1135. }
  1136.  
  1137. // reuse first element as a protoype
  1138. var combined_a = creds_a[0].cloneNode(true);
  1139. combined_a.textContent = combined_a.textContent.stripQuoteMarks();
  1140. combined_a.category = null;
  1141. combined_a.categoryList = catList;
  1142. combined_a.propertySet = creds_a[0].propertySet;
  1143.  
  1144. mergedCredits.push(combined_a);
  1145. });
  1146. return mergedCredits;
  1147. }
  1148. }
  1149.  
  1150. log.debug(
  1151. "extendImdbNameDocument: "
  1152. + "name='" + nameDoc.getName() + "'"
  1153. + ", birthYear='" + nameDoc.getBirthYear() + "'"
  1154. + ", age=" + nameDoc.getAge()
  1155. );
  1156.  
  1157. return nameDoc;
  1158. }
  1159.  
  1160.  
  1161. // --------------- IMDb Page extensions ---------------
  1162.  
  1163. function extendImdbDocument(imdbDoc)
  1164. {
  1165. extendDocument(imdbDoc);
  1166.  
  1167. addPrefsButton();
  1168.  
  1169. imdbDoc.removeAds = function()
  1170. {
  1171. log.debug("imdbDoc.removeAds");
  1172. this.foreachNode("//div[starts-with(@id, 'swf_')]", function(node) {
  1173. node.remove();
  1174. log.debug("Removing ad element: <" + node.tagName + " id=" + node.id);
  1175. }
  1176. );
  1177. // log.debug("sweeping for DOUBLECLICK:");
  1178. // this.foreachNode("//img[contains(@src, 'doubleclick.net')]/ancestor::d[1]", function(node) {
  1179. // node.remove();
  1180. // log.debug("Removing DOUBLECLICK ad div");
  1181. // }
  1182. // );
  1183. this.removeFlashAds();
  1184. // experimental
  1185. // this.foreachNode("//area[@alt='Learn more']", function(div) {
  1186. // div.remove();
  1187. // log.debug("REMOVING NAVSTRIPE AD");
  1188. // }
  1189. // );
  1190. // this.foreachNode("//a[contains(@href, '_NAVSTRIPE')]//ancestor::div[1]", function(div) {
  1191. // div.remove();
  1192. // log.debug("REMOVING NAVSTRIPE AD");
  1193. // }
  1194. // );
  1195. this.foreachNode(
  1196. "//div[starts-with(@id, 'swf_')]",
  1197. function(ad_div) {
  1198. log.debug("REMOVING FLOATER AD");
  1199. ad_div.hideNode();
  1200. }
  1201. );
  1202. // this.removeFloaterAds();
  1203. // imdbDoc.onAppears("//div[starts-with(@id, 'swf_')]", 500, function(ad_div)
  1204. // {
  1205. // log.debug("Removing floater ad");
  1206. // alert("Removing floater ad");
  1207. // ad_div.hide();
  1208. // });
  1209. }
  1210.  
  1211. imdbDoc.removeFlashAds = function()
  1212. {
  1213. this.foreachNode(
  1214. "//comment()[contains(., 'FLASH AD BEGINS')]",
  1215. function(adbegin_cmt) {
  1216. // log.info("COMMENT< '" + adbegin_cmt.textContent + "'");
  1217. // var adend_cmt = adbegin_cmt.selectNodeNullable(
  1218. // "//following-sibling::comment()[contains(., 'FLASH AD ENDS')][1]");
  1219. // if (adend_cmt != null) {
  1220. // log.info("COMMENT> '" + adend_cmt.textContent + "'");
  1221. // }
  1222. var ad_div = adbegin_cmt.nextSibling.nextSibling;
  1223. if (ad_div != null) {
  1224. log.debug("Removing ad element: <" + ad_div.tagName + " id=" + ad_div.id);
  1225. ad_div.parentNode.removeChild(ad_div);
  1226. }
  1227. }
  1228. );
  1229. }
  1230.  
  1231. // buttons
  1232. imdbDoc.addStyle(
  1233. ".iwvr_button {\n"
  1234. + " font-size: 8pt;\n"
  1235. + " font-family: Helvetica Narrow, sans-serif;\n"
  1236. + "}\n"
  1237. );
  1238.  
  1239. // research items
  1240. imdbDoc.addStyle(
  1241. "a.iwvr_researchable_item { background-color: Gold; }\n"
  1242. + "a.iwvr_researchable_item:hover { cursor: crosshair; }\n"
  1243. );
  1244.  
  1245. // research result grid styles
  1246. imdbDoc.addStyle(
  1247. ".iwvr_results {\n"
  1248. + " padding: 3;\n"
  1249. + " font-family: Arial Narrow, Helvetica Narrow, sans-serif;\n"
  1250. + " font-size: small;\n"
  1251. + "}\n"
  1252.  
  1253. + "table.iwvr_results {\n"
  1254. + " border-collapse: collapse;\n"
  1255. + " font-family: Arial, Helvetica, sans-serif;\n"
  1256. + "}\n"
  1257.  
  1258. + "td.iwvr_results {\n"
  1259. + " border: 1px solid Gray;\n"
  1260. + " padding: 3;\n"
  1261. + "}\n"
  1262.  
  1263. + "div.iwvr_results_legend {\n"
  1264. + " max-width: 100%;\n"
  1265. + " text-align: center;\n"
  1266. + "}\n"
  1267.  
  1268. + "td.bulletcol { text-align: right; }\n"
  1269. + "td.titlecol { text-align: left; }\n"
  1270. + "td.datacol { text-align: center; }\n"
  1271. );
  1272.  
  1273. return imdbDoc;
  1274. }
  1275.  
  1276. // --------------- helper functions ---------------
  1277.  
  1278. function createImageMagnifiers(theDoc, imgUrlContains, imgUrlRegex, imgUrlReplacement)
  1279. {
  1280. // Headshot Magnifier
  1281. theDoc.foreachNode("//img[contains(@src, '" + imgUrlContains + "')]", function(img) {
  1282. if (img.src.indexOf("addtiny.gif") != -1) {
  1283. return; // skip place-holders
  1284. }
  1285. // substitute the larger image
  1286. img.src = img.src.replace(imgUrlRegex, imgUrlReplacement);
  1287. img.className = "iwvr_headshot";
  1288. img.height = null;
  1289. img.width = null;
  1290. });
  1291. theDoc.addStyle(
  1292. "img.iwvr_headshot { height: 30px; width: 23px; }\n"
  1293. + "td:hover img.iwvr_headshot {\n"
  1294. + " height: auto;\n"
  1295. + " width: auto;\n"
  1296. + " z-index: 999;\n"
  1297. + " position: fixed;\n"
  1298. + " top: 5%;\n"
  1299. + " right: 5%;\n"
  1300. + "}\n"
  1301. );
  1302. // zoom visualization graphic
  1303. // var zoom_img = dm.xdoc.createXElement("img");
  1304. // with (zoom_img.style) {
  1305. // position = "absolute";
  1306. // top = "614px";
  1307. // left = "145px";
  1308. // }
  1309. // var zoom_img_src = 'data:image/gif;base64,' +
  1310. // 'R0lGODlhGACCAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/////' +
  1311. // '/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
  1312. // 'AAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBmZgBm' +
  1313. // 'mQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/mQD/zAD/' +
  1314. // '/zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNmzDNm/zOZADOZ' +
  1315. // 'MzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYA' +
  1316. // 'mWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZAGaZM2aZZmaZmWaZzGaZ' +
  1317. // '/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkz' +
  1318. // 'M5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnM' +
  1319. // 'mZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz' +
  1320. // '/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/' +
  1321. // 'M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9m' +
  1322. // 'mf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP//' +
  1323. // '/ywAAAAAGACCAAAI/wAfCBxIsKBBgggOKlSIIOHChwIbQoTY0OHEgxUvMsyosWBFix0jcgwpcmTIjyA7' +
  1324. // 'okx5cSXJBy5JrmRJMabKmSdx3tTZcibNjTwn+pTY06fGoURrDi26VGnTh0iTLowqFSNVp0ihUv1Z8ipQ' +
  1325. // 'r1a3Tt1adSDZsl3FGjyLli1NtyzhrpWLkK5ZuCDx5sVbl29atnf1/gUMU69Dw4cNDz67mGxjtYgfe41c' +
  1326. // 'WHBlv5QzK75MV7Nlz5g3g+4suvRn06FPq07N2m1gu5YlP+XM+HVt24773sYNlnfWubt9zxYeVHdv47/D' +
  1327. // 'HkdudOxy5sU9PoeOEmt04MOVX8e+XXpzodm/2i60bpI8WvHlzeesvv6jzPFMz2t1/z59/JeX8efHT/+l' +
  1328. // '/aPygaefSAPCVOCBDwQEADs=';
  1329. // zoom_img.src = zoom_img_src;
  1330. // titleDoc.body.appendChild(zoom_img);
  1331. // zoom_img.hide();
  1332. }
  1333.  
  1334.  
  1335. // ==================== AbbreviationMap object ====================
  1336.  
  1337. function AbbreviationMap(initMap)
  1338. {
  1339. this.map = initMap;
  1340.  
  1341. if (this.map == null) {
  1342. this.map = new Array();
  1343. }
  1344.  
  1345. this.register = function(value)
  1346. {
  1347. var abbrev = arrayIndexOf(this.map, value);
  1348. if (abbrev != null)
  1349. return abbrev;
  1350.  
  1351. for (var len = 1; len < value.length; len++)
  1352. {
  1353. var abbrev = value.substring(0, len);
  1354. if (this.map[abbrev] == null) {
  1355. this.map[abbrev] = value;
  1356. return abbrev;
  1357. }
  1358. }
  1359. throw "Can't abbreviate: '" + value + "'";
  1360. }
  1361.  
  1362. this.registerList = function(theList)
  1363. {
  1364. var abbrevList = new Array();
  1365. for (var i in theList) {
  1366. abbrevList.push(this.register(theList[i]));
  1367. }
  1368. return abbrevList;
  1369. }
  1370.  
  1371. this.toLegend = function(elemType, attrMap)
  1372. {
  1373. // var dm.xdoc = extendDocument(document);
  1374. var node = dm.xdoc.createXElement(elemType, attrMap);
  1375. with (node) {
  1376. for (var i in this.map) {
  1377. appendChildText(i);
  1378. appendChildText(':\u00A0"');
  1379. appendChildText(this.map[i]);
  1380. appendChildText('"');
  1381. appendChildText(' - ');
  1382. }
  1383. }
  1384. return node;
  1385. }
  1386. }
  1387.  
  1388.  
  1389. // ==================== Preferences Dialog ====================
  1390.  
  1391. function addPrefsButton()
  1392. {
  1393. configurePrefsButton(function(prefsMgr, prefsDialog_div)
  1394. {
  1395. var table = dm.xdoc.createXElement("table");
  1396. prefsDialog_div.appendChild(table);
  1397.  
  1398. var tr = dm.xdoc.createXElement("tr");
  1399. table.appendChild(tr);
  1400.  
  1401. var td = dm.xdoc.createXElement("td");
  1402. td.style.verticalAlign = "top";
  1403. tr.appendChild(td);
  1404. with (td)
  1405. {
  1406. var features_div = dm.xdoc.createTopicDiv("Enabled Features", td);
  1407. appendChild(features_div);
  1408. with (features_div.contentElement)
  1409. {
  1410. var genFeatures_div = dm.xdoc.createTopicDiv("All Pages", features_div);
  1411. appendChild(genFeatures_div);
  1412. with (genFeatures_div.contentElement)
  1413. {
  1414. appendChild(prefsMgr.createPreferenceInput(
  1415. "highlightTitleTypes",
  1416. "Highlight titles by type",
  1417. "white: theatrical release / gold: direct to video / blue: TV / pink: video game"
  1418. ));
  1419. appendChildElement("br");
  1420. appendChild(prefsMgr.createPreferenceInput(
  1421. "removeAds",
  1422. "Remove advertising",
  1423. "Remove advertising"
  1424. ));
  1425. }
  1426.  
  1427. var titleFeatures_div = dm.xdoc.createTopicDiv("On Title Pages", features_div);
  1428. appendChild(titleFeatures_div);
  1429. with (titleFeatures_div.contentElement)
  1430. {
  1431. appendChild(prefsMgr.createPreferenceInput(
  1432. "title-attributes",
  1433. "Display title attributes",
  1434. "Display rating/runtime/language directly below title"
  1435. ));
  1436. appendChildElement("br");
  1437. appendChild(prefsMgr.createPreferenceInput(
  1438. "title-ShowAges",
  1439. "[Show Ages] button",
  1440. "Compute the ages of cast members"
  1441. ));
  1442. appendChildElement("br");
  1443. appendChild(prefsMgr.createPreferenceInput(
  1444. "title-StartResearch",
  1445. "[Start Research] button",
  1446. "Open the Research dialog"
  1447. ));
  1448. appendChildElement("br");
  1449. appendChild(prefsMgr.createPreferenceInput(
  1450. "title-headshotMagnifier",
  1451. "Headshot Magnifier",
  1452. "Hover mouse to magnify cast pictures"
  1453. ));
  1454. appendChildElement("br");
  1455. appendChild(prefsMgr.createPreferenceInput(
  1456. "title-headshotMagnification",
  1457. "Mag level",
  1458. "Magnification factor",
  1459. { size:2, maxLength: 2 }
  1460. )).style.marginLeft = "28px";
  1461. }
  1462.  
  1463. var nameFeatures_div = dm.xdoc.createTopicDiv("On Name Pages", features_div);
  1464. appendChild(nameFeatures_div);
  1465. with (nameFeatures_div.contentElement)
  1466. {
  1467. appendChild(prefsMgr.createPreferenceInput(
  1468. "name-ShowAge",
  1469. "Display age",
  1470. "Display current age of the person"
  1471. ));
  1472. appendChildElement("br");
  1473. appendChild(prefsMgr.createPreferenceInput(
  1474. "name-ShowAgeAtTitleRelease",
  1475. "Display age at release",
  1476. "Display age at time title was released"
  1477. ));
  1478. }
  1479.  
  1480. var findFeatures_div = dm.xdoc.createTopicDiv("On Search Results", features_div);
  1481. appendChild(findFeatures_div);
  1482. with (findFeatures_div.contentElement)
  1483. {
  1484. appendChild(prefsMgr.createPreferenceInput(
  1485. "firstMatchAccessKey",
  1486. "Select first match",
  1487. "Alt-Shift keyboard to navigate to first title matched",
  1488. { size:1, maxLength: 1 }
  1489. ));
  1490. }
  1491. }
  1492. }
  1493.  
  1494. var td = dm.xdoc.createXElement("td");
  1495. td.style.verticalAlign = "top";
  1496. tr.appendChild(td);
  1497. with (td) {
  1498. appendChild(prefsMgr.constructDockPrefsMenuSection(td));
  1499. appendChild(prefsMgr.constructAdvancedControlsSection(td));
  1500.  
  1501. var controls_div = dm.xdoc.createTopicDiv("Performance Controls", td);
  1502. with (controls_div.contentElement)
  1503. {
  1504. appendChild(prefsMgr.createPreferenceInput(
  1505. "ajaxOperationLimit",
  1506. "Background threads",
  1507. "Control how many simultaneous background operations are allowed"
  1508. + ", (primarily affects Show Ages)",
  1509. { size:1, maxLength: 2 }
  1510. ));
  1511. }
  1512. appendChild(controls_div);
  1513. }
  1514.  
  1515. // Help link
  1516. var docs_div = dm.xdoc.createXElement("div");
  1517. prefsDialog_div.appendChild(docs_div);
  1518. with (docs_div) {
  1519. appendChild(dm.xdoc.createHtmlLink(
  1520. "http://refactoror.com/greasemonkey/imdbWeaver/doc.html#prefs",
  1521. "Help"
  1522. ));
  1523. align = "center";
  1524. style.padding = "3px";
  1525. }
  1526. });
  1527. }
  1528.  
  1529.  
  1530. // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  1531. // =-=-=-=-=-=-=-=-=-=-= refactoror lib -=-=-=-=-=-=-=-=-=-=-=
  1532. // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  1533.  
  1534. // common logic for the way I like to setup Preferences in my apps
  1535. // Requires preferences: prefsMenuAccessKey, prefsMenuPosition, prefsMenuVisible, loggerLevel
  1536. function configurePrefsButton(dialogConstructor)
  1537. {
  1538. // Preferences dialog
  1539. GM_registerMenuCommand(dm.metadata["name"] + " Preferences...", openPrefsDialog);
  1540. createPrefsButton();
  1541.  
  1542. // Prefs dialog
  1543. function createPrefsButton()
  1544. {
  1545. var menuButton = dm.xdoc.createXElement("button", { textContent: "Prefs" });
  1546. setScreenPosition(menuButton, prefs.get("prefsMenuPosition"));
  1547. if (prefs.get("prefsMenuVisible") == false) {
  1548. menuButton.style.opacity = 0; // active but not visibile
  1549. menuButton.style.zIndex = -1; // don't block other content
  1550. }
  1551.  
  1552. with (menuButton) {
  1553. id = dm.metadata["moniker"] + "_prefs_menu_button";
  1554. title = dm.metadata["name"] + " Preferences";
  1555. style.fontSize = "9pt";
  1556. addEventListener('click', openPrefsDialog, false);
  1557. // accessKey = getDeconflicted("prefsMenuAccessKey", "accessKey");
  1558. accessKey = prefs.get("prefsMenuAccessKey");
  1559. }
  1560. if (dm.xdoc.body != null) {
  1561. dm.xdoc.body.appendChild(menuButton);
  1562. }
  1563. }
  1564.  
  1565. function getDeconflicted(prefsName, attrName)
  1566. {
  1567. var prefValue = prefs.get(prefsName);
  1568. var node = xdoc.selectNodeNullable("//*[@" + attrName + "='" + prefValue + "']");
  1569. if (node != null) {
  1570. log.warn("Conflict: <" + node.nodeName + "> element on this page is already using "
  1571. + attrName + "=" + prefValue);
  1572. prefValue = null;
  1573. }
  1574. return prefValue;
  1575. }
  1576.  
  1577. // Prefs dialog
  1578. function openPrefsDialog(event)
  1579. {
  1580. var prefsMgr = new PreferencesManager(
  1581. dm.xdoc,
  1582. dm.metadata["moniker"] + "_prefs",
  1583. dm.metadata["name"] + " Preferences",
  1584. { OK: function okPrefs(doc) { prefsMgr.storePrefs(); },
  1585. Cancel: noop
  1586. }
  1587. );
  1588. var prefsDialog_div = prefsMgr.open();
  1589. if (prefsDialog_div == null)
  1590. return; // the dialog is already open
  1591.  
  1592. prefsMgr.constructDockPrefsMenuSection = function(contextNode)
  1593. {
  1594. var prefsDock_div = dm.xdoc.createTopicDiv("Dock [Prefs] Menu", contextNode);
  1595. contextNode.style.verticalAlign = "top";
  1596. with (prefsDock_div.contentElement)
  1597. {
  1598. appendChild(prefsMgr.createPreferenceInput(
  1599. "prefsMenuVisible",
  1600. "Visible",
  1601. "Prefs menu button visible on the screen"
  1602. ));
  1603. with (appendChild(prefsMgr.createScreenCornerPreference("prefsMenuPosition"))) {
  1604. title = "Screen corner for [Prefs] menu button";
  1605. style.margin = "1px 0px 3px 20px";
  1606. }
  1607. appendChild(prefsMgr.createPreferenceInput(
  1608. "prefsMenuAccessKey",
  1609. "Access Key",
  1610. "Alt-Shift keyboard shortcut",
  1611. { size:1, maxLength: 1 }
  1612. ));
  1613. }
  1614. return prefsDock_div;
  1615. }
  1616.  
  1617. prefsMgr.constructAdvancedControlsSection = function(contextNode)
  1618. {
  1619. var controls_div = dm.xdoc.createTopicDiv("Advanced Controls", contextNode);
  1620. with (controls_div.contentElement)
  1621. {
  1622. appendChild(prefsMgr.createPreferenceInput(
  1623. "loggerLevel",
  1624. "Logging Level",
  1625. "Control level of information that appears in the Error Console",
  1626. null,
  1627. log.getLogLevelMap()
  1628. ));
  1629. }
  1630. return controls_div;
  1631. }
  1632.  
  1633. dialogConstructor(prefsMgr, prefsDialog_div);
  1634. }
  1635.  
  1636. dispatchFeature("sendAnonymousStatistics", function() {
  1637. if (getElapsed("sendAnonymousStatistics") < 2000) {
  1638. log.debug("--------- SKIPPING COUNTER on rapid fire: " + dm.xdoc.location.href);
  1639. return;
  1640. }
  1641. log.debug("--------- EMBEDDING COUNTER: " + dm.xdoc.location.href);
  1642. // var counter_img = document.createElement("img");
  1643. // counter_img.id = "refactoror.net_counter";
  1644. // counter_img.src = "http://refactoror.net/spacer.gif?"
  1645. // + dm.metadata["moniker"] + "ver=" + dm.metadata["version"]
  1646. // + "&od=" + GM_getValue("odometer")
  1647. // ;
  1648. // log.debug(counter_img.src + " :: location=" + document.location.href);
  1649. // dm.xdoc.body.appendChild(counter_img);
  1650. });
  1651.  
  1652. function getElapsed(name) {
  1653. var prev_ms = parseInt(GM_getValue(name + "_ms", "0"));
  1654. var now_ms = Number(new Date());
  1655. GM_setValue(name + "_ms", now_ms.toString());
  1656.  
  1657. return (now_ms - prev_ms);
  1658. }
  1659. }
  1660.  
  1661.  
  1662. // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  1663. // =-=-=-=-=-=-=-=-=-=-=-= DOM Monkey -=-=-=-=-=-=-=-=-=-=-=-=
  1664. // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  1665.  
  1666. /* Parses the script headers into the metadata object.
  1667. * Adds constants & utility methods to various javascript objects.
  1668. * Initializes the Preferences object.
  1669. * Initializes the logger object.
  1670. */
  1671. function DomMonkey(metadata)
  1672. {
  1673. extendJavascriptObjects();
  1674.  
  1675. // DM objects provided on the context
  1676.  
  1677. this.xdoc = extendDocument(document);
  1678. this.metadata = metadata;
  1679.  
  1680. // The values listed here are the first-time-use defaults
  1681. // They have no effect once they are stored as mozilla preferences.
  1682. prefs = new Preferences({
  1683. "loggerLevel": "WARN"
  1684. ,"sendAnonymousStatistics": true
  1685. });
  1686.  
  1687. log = new Logger(this.metadata["version"]);
  1688.  
  1689. GM_setValue("odometer", GM_getValue("odometer", 0) + 1);
  1690. }
  1691.  
  1692.  
  1693. // ==================== DOM object extensions ====================
  1694.  
  1695. /** Extend the given document with methods
  1696. * for querying and modifying the document object.
  1697. */
  1698. function extendDocument(doc)
  1699. {
  1700. if (doc == null)
  1701. return null;
  1702.  
  1703. /** Determine if the current document is empty.
  1704. */
  1705. doc.isEmpty = function() {
  1706. return (this.body == null || this.body.childNodes.length == 0);
  1707. };
  1708.  
  1709. /** Report number of nodes that matach the given xpath expression.
  1710. */
  1711. doc.countNodes = function(xpath) {
  1712. var n = 0;
  1713. this.foreachNode(xpath, function(node) {
  1714. n++;
  1715. });
  1716. return n;
  1717. };
  1718.  
  1719. /** Remove nodes that match the given xpath expression.
  1720. */
  1721. doc.removeNodes = function(xpath) {
  1722. this.foreachNode(xpath, function(node) {
  1723. node.remove();
  1724. });
  1725. };
  1726.  
  1727. /** Hide nodes that match the given xpath expression.
  1728. */
  1729. doc.hideNodes = function(xpath)
  1730. {
  1731. if (xpath instanceof Array) {
  1732. for (var xp in xpath) {
  1733. this.foreachNode(xp, function(node) {
  1734. node.hide();
  1735. });
  1736. }
  1737. }
  1738. else {
  1739. this.foreachNode(xpath, function(node) {
  1740. node.hide();
  1741. });
  1742. }
  1743. };
  1744.  
  1745. /** Make visible the nodes that match the given xpath expression.
  1746. */
  1747. doc.showNodes = function(xpath) {
  1748. this.foreachNode(xpath, function(node) {
  1749. node.show();
  1750. });
  1751. };
  1752.  
  1753. /** Retrieve the value of the node that matches the given xpath expression.
  1754. */
  1755. doc.selectValue = function(xpath, contextNode)
  1756. {
  1757. if (contextNode == null)
  1758. contextNode = this;
  1759.  
  1760. var result = this.evaluate(xpath, contextNode, null, XPathResult.ANY_TYPE, null);
  1761. var resultVal;
  1762. switch (result.resultType) {
  1763. case result.STRING_TYPE: resultVal = result.stringValue; break;
  1764. case result.NUMBER_TYPE: resultVal = result.numberValue; break;
  1765. case result.BOOLEAN_TYPE: resultVal = result.booleanValue; break;
  1766. default:
  1767. log.error("Unhandled value type: " + result.resultType);
  1768. }
  1769. return resultVal;
  1770. }
  1771.  
  1772. /** Select the first node that matches the given xpath expression.
  1773. * If none found, log warning and return null.
  1774. */
  1775. doc.selectNode = function(xpath, contextNode)
  1776. {
  1777. var node = this.selectNodeNullable(xpath, contextNode);
  1778. if (node == null) {
  1779. // is it possible that the structure of this web page has changed?
  1780. log.warn("XPath returned no elements: " + xpath
  1781. + "\n" + genStackTrace(arguments.callee)
  1782. );
  1783. }
  1784. return node;
  1785. }
  1786.  
  1787. /** Select the first node that matches the given xpath expression.
  1788. * If none found, return null.
  1789. */
  1790. doc.selectNodeNullable = function(xpath, contextNode)
  1791. {
  1792. if (contextNode == null)
  1793. contextNode = this;
  1794.  
  1795. var resultNode = this.evaluate(
  1796. xpath, contextNode, null,
  1797. XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  1798.  
  1799. if (resultNode.singleNodeValue == null)
  1800. log.debug("null result for: " + xpath);
  1801. return extendNode(resultNode.singleNodeValue);
  1802. }
  1803.  
  1804. /** Select all first nodes that match the given xpath expression.
  1805. * If none found, return an empty Array.
  1806. */
  1807. doc.selectNodes = function(xpath, contextNode)
  1808. {
  1809. var nodeList = new Array();
  1810. this.foreachNode(xpath, function(n) { nodeList.push(n); }, contextNode);
  1811. return nodeList;
  1812. }
  1813.  
  1814. /** Select all nodes that match the given xpath expression.
  1815. * If none found, return null.
  1816. */
  1817. doc.selectNodeSet = function(xpath, contextNode)
  1818. {
  1819. if (contextNode == null)
  1820. contextNode = this;
  1821.  
  1822. var nodeSet = this.evaluate(
  1823. xpath, contextNode, null,
  1824. XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  1825.  
  1826. return nodeSet;
  1827. }
  1828.  
  1829. /** Iteratively execute the given func for each node that matches the given xpath expression.
  1830. */
  1831. doc.foreachNode = function(xpath, func, contextNode)
  1832. {
  1833. if (contextNode == null)
  1834. contextNode = this;
  1835.  
  1836. // if array of xpath strings, call recursively
  1837. if (xpath instanceof Array) {
  1838. for (var i=0; i < xpath.length; i++)
  1839. this.foreachNode(xpath[i], func, contextNode);
  1840. return;
  1841. }
  1842.  
  1843. var nodeSet = contextNode.selectNodeSet(xpath, contextNode);
  1844.  
  1845. var i = 0;
  1846. var n = nodeSet.snapshotItem(i);
  1847. while (n != null) {
  1848. var result = func(extendNode(n));
  1849. if (result == false) {
  1850. // dispatching func can abort the loop by returning false
  1851. return;
  1852. }
  1853. n = nodeSet.snapshotItem(++i);
  1854. }
  1855. }
  1856.  
  1857. /** Retrieve the text content of the node that matches the given xpath expression.
  1858. */
  1859. doc.selectTextContent = function(xpath) {
  1860. var node = this.selectNodeNullable(xpath, this);
  1861. if (node == null)
  1862. return null;
  1863. return node.textContent.normalizeWhitespace();
  1864. };
  1865.  
  1866. /** Retrieve the text content of the node that matches the given xpath expression,
  1867. * and apply the given regular expression to it, returning the portion that matches.
  1868. */
  1869. doc.selectMatchTextContent = function(xpath, regex) {
  1870. var text = this.selectTextContent(xpath);
  1871. if (text == null)
  1872. return null;
  1873. return text.match(regex);
  1874. };
  1875.  
  1876. /** Replace contents of contextNode (default: body), with specified node.
  1877. * (The specified node is removed, then re-added to the emptied contextNode.)
  1878. * The specified node is expected to be a descendent of the context node.
  1879. * Otherwise the result is probably an error.
  1880. * DOC-DEFAULT
  1881. */
  1882. doc.isolateNode = function(xpath, contextNode)
  1883. {
  1884. if (contextNode == null)
  1885. contextNode = this.body;
  1886.  
  1887. extendNode(contextNode);
  1888.  
  1889. var subjectNode = this.selectNode(xpath);
  1890. if (subjectNode == null || subjectNode.parentNode == null)
  1891. return;
  1892.  
  1893. // gut the parent node (leave script elements alone)
  1894. contextNode.foreachNode("child::*", function(node) {
  1895. if (node.tagName != "SCRIPT" && node.tagName != "NOSCRIPT") {
  1896. node.remove();
  1897. }
  1898. });
  1899.  
  1900. // re-add the subject node
  1901. var replacement_div = this.createElement("div");
  1902. replacement_div.id = "isolateNode:" + xpath;
  1903. replacement_div.appendChild(subjectNode);
  1904.  
  1905. contextNode.appendChild(replacement_div);
  1906. return replacement_div;
  1907. };
  1908.  
  1909. /** Add a <script> reference to this document.
  1910. * DOC-CENTRIC
  1911. */
  1912. doc.addScriptReference = function(url)
  1913. {
  1914. var script = this.createElement("script");
  1915. script.src = url;
  1916. this.selectNode("//head").appendChild(script);
  1917.  
  1918. return script;
  1919. }
  1920.  
  1921. /** Add a CSS style definition to this document.
  1922. * DOC-CENTRIC
  1923. */
  1924. doc.addStyle = function(cssBody, id)
  1925. {
  1926. var style = this.createXElement("style");
  1927. style.innerHTML = cssBody;
  1928. this.selectNode("//head").appendChild(style);
  1929.  
  1930. return style;
  1931. }
  1932.  
  1933. /** Create an "extended" HTML element of the specified type,
  1934. * with the given attributes applied to it.
  1935. * The returned object is extended by extendNode().
  1936. * DOC-NONSPECIFIC
  1937. */
  1938. doc.createXElement = function(tagName, attrMap)
  1939. {
  1940. var node = extendNode(this.createElement(tagName));
  1941. node.applyAttributes(attrMap);
  1942. return node;
  1943. }
  1944.  
  1945. /** Create
  1946. */
  1947. doc.createHtmlLink = function(url, text, attrMap)
  1948. {
  1949. var a = this.createXElement("a");
  1950. a.href = url;
  1951. if (text == null) {
  1952. text = url;
  1953. }
  1954. a.textContent = text;
  1955. a.applyAttributes(attrMap);
  1956. return a;
  1957. }
  1958.  
  1959. /** Create an HTML input field, wrapped in an HTML label,
  1960. * with the given attributes applied to it,
  1961. * The returned HTML objects are extended by extendNode().
  1962. * DOC-NONSPECIFIC
  1963. */
  1964. doc.createInputText = function(labelText, attrMap, defaultVal)
  1965. {
  1966. var span = this.createXElement("label");
  1967. with (span) {
  1968. if (labelText != null)
  1969. appendChildText(labelText + ": ");
  1970. var input = this.createXElement("input", attrMap);
  1971. with (input) {
  1972. type = "text";
  1973. value = defaultVal;
  1974. }
  1975. appendChild(input);
  1976. }
  1977. return span;
  1978. }
  1979.  
  1980. doc.createTextArea = function(labelText, attrMap, defaultVal)
  1981. {
  1982. var span = this.createXElement("label");
  1983. with (span) {
  1984. if (labelText != null)
  1985. appendChildText(labelText + ": ");
  1986. var input = this.createXElement("textarea", attrMap);
  1987. with (input) {
  1988. value = defaultVal;
  1989. }
  1990. appendChild(input);
  1991. }
  1992. return span;
  1993. }
  1994.  
  1995. /** Create an HTML checkbox, wrapped in an HTML label,
  1996. * with the given attributes applied to it,
  1997. * The returned HTML objects are extended by extendNode().
  1998. * DOC-NONSPECIFIC
  1999. */
  2000. doc.createCheckbox = function(labelText, attrMap, isChecked)
  2001. {
  2002. var span = this.createXElement("label");
  2003. with (span) {
  2004. var input = this.createXElement("input", attrMap);
  2005. with (input) {
  2006. type = "checkbox";
  2007. checked = isChecked;
  2008. }
  2009. appendChild(input);
  2010. appendChildText(labelText);
  2011. }
  2012. return span;
  2013. }
  2014.  
  2015. /** Create a set of HTML radio buttons, wrapped in an HTML label element.
  2016. * The returned HTML objects are extended by extendNode().
  2017. * DOC-NONSPECIFIC
  2018. */
  2019. doc.createRadioset = function(attrMap, optionMap, defaultKey)
  2020. {
  2021. var spanList = new Array();
  2022.  
  2023. for (var key in optionMap)
  2024. {
  2025. var label = this.createXElement("label");
  2026. with (label) {
  2027. var input = this.createXElement("input", attrMap);
  2028. with (input) {
  2029. type = "radio";
  2030. value = key;
  2031. if (key == defaultKey)
  2032. checked = true;
  2033. }
  2034. appendChild(input);
  2035. appendChildText(optionMap[key]);
  2036. }
  2037. spanList.push(label);
  2038. }
  2039. return spanList;
  2040. }
  2041.  
  2042. /** Create an HTML select element, wrapped in an HTML label element.
  2043. * The returned HTML objects are extended by extendNode().
  2044. * DOC-NONSPECIFIC
  2045. */
  2046. doc.createSelect = function(labelText, attrMap, optionMap, defaultKey)
  2047. {
  2048. var span = this.createXElement("label");
  2049. with (span) {
  2050. if (labelText != null)
  2051. appendChildText(labelText + ": ");
  2052. var select = this.createXElement("select", attrMap);
  2053. with (select)
  2054. {
  2055. for (var key in optionMap)
  2056. {
  2057. var option = this.createXElement("option");
  2058. with (option) {
  2059. value = key;
  2060. if (key == defaultKey) {
  2061. selected = true;
  2062. }
  2063. appendChildText(optionMap[key]);
  2064. }
  2065. appendChild(option);
  2066. }
  2067. }
  2068. appendChild(select);
  2069. }
  2070. return span;
  2071. }
  2072.  
  2073. /** Create a labeled/boxed area (eg, typical dialog box component).
  2074. */
  2075. doc.createTopicDiv = function(topicTitle, contextNode)
  2076. {
  2077. var shiftEms = ".7";
  2078. var basecolor = getBaseColor(contextNode);
  2079.  
  2080. var frame_div = this.createXElement("div");
  2081. with (frame_div) {
  2082. with (style) {
  2083. border = "1px solid Gray";
  2084. marginTop = (shiftEms * 1.5) + "em";
  2085. marginLeft = "6px";
  2086. marginRight = "6px";
  2087. MozBorderRadius = "3px";
  2088. }
  2089.  
  2090. // superimposed title
  2091. var title_span = this.createXElement("span");
  2092. with (title_span.style) {
  2093. position = "relative";
  2094. top = -shiftEms + "em";
  2095. fontSize = "10pt";
  2096. color = "Black";
  2097. backgroundColor = basecolor;
  2098. marginLeft = "6px"; // shift title right
  2099. padding = "0px 4px 0px 4px"; // blot out frame on left & right
  2100. }
  2101. title_span.appendChildText(topicTitle);
  2102. appendChild(title_span);
  2103. // maintatin default mouse cursor over the topic label text
  2104. title_span.wrapIn("label");
  2105.  
  2106. // content area
  2107. var content_div = this.createXElement("div");
  2108. content_div.style.marginTop = -shiftEms + "em";
  2109. content_div.style.padding = "6px";
  2110. appendChild(content_div);
  2111. }
  2112. frame_div.contentElement = content_div;
  2113.  
  2114. return frame_div;
  2115.  
  2116. function getBaseColor(contextNode)
  2117. {
  2118. while (contextNode != null && contextNode.tagName != "BODY") {
  2119. var c = contextNode.style.backgroundColor;
  2120. if (c != "") {
  2121. return c;
  2122. }
  2123. contextNode = contextNode.parentNode;
  2124. }
  2125. return "White";
  2126. }
  2127. }
  2128.  
  2129. return doc;
  2130. }
  2131.  
  2132. /** Extend the given node with methods
  2133. * for querying and modifying the node object.
  2134. */
  2135. function extendNode(node)
  2136. {
  2137. if (node == null)
  2138. return null;
  2139.  
  2140. /** Create an HTML element of the specified type,
  2141. * with the given attributes applied to it.
  2142. * The returned object is extended by extendNode().
  2143. */
  2144. node.createXElement = function(tagName, attrMap)
  2145. {
  2146. var node = extendNode(this.ownerDocument.createElement(tagName));
  2147. this.applyAttributes(attrMap);
  2148. return node;
  2149. }
  2150.  
  2151. // Selection methods that operate within the scope of this node
  2152.  
  2153. node.selectValue = function(xpath) { return document.selectValue(xpath, this); }
  2154. node.selectNode = function(xpath) { return document.selectNode(xpath, this); }
  2155. node.selectNodeNullable = function(xpath) { return document.selectNodeNullable(xpath, this); }
  2156. node.selectNodeSet = function(xpath) { return document.selectNodeSet(xpath, this); }
  2157.  
  2158. node.foreachNode = function(xpath, func) { document.foreachNode(xpath, func, this); }
  2159. node.isolateNode = function(xpath) { document.isolateNode(xpath, this); }
  2160.  
  2161. node.applyAttributes = function(attrMap) {
  2162. for (var key in attrMap) {
  2163. this[key] = attrMap[key];
  2164. }
  2165. }
  2166.  
  2167. /** &nbsp;
  2168. */
  2169. node.NBSP = "\u00A0";
  2170.  
  2171. /** Create a DOM object of the given type,
  2172. * and append it to this node.
  2173. */
  2174. node.appendChildElement = function(tagName) {
  2175. var newNode = this.createXElement(tagName);
  2176. this.appendChild(newNode);
  2177. return newNode;
  2178. };
  2179.  
  2180. /** Create a text node,
  2181. * optionally wrapped in the given HTML element types,
  2182. * and append it to this node.
  2183. */
  2184. node.appendChildText = function(text, spanList, attrMap)
  2185. {
  2186. var newNode = this.ownerDocument.createTextNode(text);
  2187. // wrap with other elements, if any, (eg: ["b", "i"])
  2188. if (spanList != null) {
  2189. for (var i = spanList.length - 1; i >= 0; i--) {
  2190. var n = this.createXElement(spanList[i]);
  2191. n.appendChild(newNode);
  2192. newNode = n;
  2193. }
  2194. }
  2195. if (attrMap != null) {
  2196. newNode.applyAttributes(attrMap);
  2197. }
  2198. this.appendChild(newNode);
  2199. return newNode;
  2200. };
  2201.  
  2202. /** Create a text node consisting of a series of &nbsp; entities,
  2203. * and append it to this node.
  2204. */
  2205. node.appendChildTextNbsp = function(count) {
  2206. if (count == null)
  2207. count = 1;
  2208. var buf = "";
  2209. for (var i = 0; i < count; i++) {
  2210. buf += this.NBSP;
  2211. }
  2212. return this.appendChildText(buf);
  2213. };
  2214.  
  2215. /** Insert the given node as the first child of this node.
  2216. */
  2217. node.prependChild = function(newNode) {
  2218. this.insertBefore(newNode, this.firstChild);
  2219. return newNode;
  2220. };
  2221.  
  2222. /** Insert the given node in front of this node.
  2223. */
  2224. node.prependSibling = function(newNode) {
  2225. var p = this.parentNode;
  2226. p.insertBefore(newNode, this);
  2227. return newNode;
  2228. };
  2229.  
  2230. /** Insert the given node after this node.
  2231. */
  2232. node.appendSibling = function(newNode) {
  2233. var p = this.parentNode;
  2234. var followingSibling = this.nextSibling;
  2235. p.insertBefore(newNode, followingSibling);
  2236. return newNode;
  2237. };
  2238.  
  2239. /** Create an HTML element of the specified type,
  2240. * with the given attributes applied to it,
  2241. * then move this node inside the newly created node,
  2242. * and attach the newly created node in place of this node
  2243. * returning the newly created object.
  2244. */
  2245. node.wrapIn = function(type, attrs) {
  2246. var wrapperNode = this.createXElement(type, attrs);
  2247. this.prependSibling(wrapperNode);
  2248. this.remove();
  2249. wrapperNode.appendChild(this);
  2250. return wrapperNode;
  2251. };
  2252.  
  2253. /**
  2254. */
  2255. node.makeCollapsible = function(id, isPersistent, isInitExpanded) {
  2256. return new Collapsible(this, id, isPersistent, isInitExpanded);
  2257. };
  2258.  
  2259. /** Remove this node, and insert the given node in its place.
  2260. * .. more details
  2261. */
  2262. node.replaceWith = function(node) {
  2263. this.appendSibling(node);
  2264. this.remove();
  2265. return node;
  2266. };
  2267.  
  2268. /** Create an HTML table row.
  2269. * .. more details
  2270. */
  2271. node.appendTableRow = function(valueList, tdAttrMapList)
  2272. {
  2273. var tr = this.createXElement("tr");
  2274. for (var i in valueList)
  2275. {
  2276. var td = this.createXElement("td");
  2277. if (tdAttrMapList != null)
  2278. td.applyAttributes(tdAttrMapList[i]);
  2279. if (valueList[i] == null)
  2280. ;
  2281. else if (typeof(valueList[i]) == "string")
  2282. td.appendChild( this.ownerDocument.createTextNode(valueList[i]) );
  2283. else
  2284. td.appendChild( valueList[i] );
  2285. tr.appendChild(td);
  2286. }
  2287. this.appendChild(tr);
  2288. }
  2289.  
  2290. /** Remove this node from the DOM.
  2291. */
  2292. node.remove = function() {
  2293. this.parentNode.removeChild(this);
  2294. return this;
  2295. }
  2296.  
  2297. /** Hide this node.
  2298. */
  2299. node.hide = function() {
  2300. this.style.display = "none";
  2301. }
  2302.  
  2303. /** Hide nodes that are siblings to this node.
  2304. */
  2305. node.hideSiblings = function() {
  2306. this.foreachNode("../child::*", function(node) {
  2307. if (! this.isSameNode(node)) {
  2308. if (node.tagName != "SCRIPT" && node.tagName != "NOSCRIPT")
  2309. node.hide();
  2310. }
  2311. });
  2312. };
  2313.  
  2314. /** Show this node.
  2315. */
  2316. node.show = function() {
  2317. this.style.display = null;
  2318. }
  2319.  
  2320. /** Calculate the absolute X position of this HTML element.
  2321. */
  2322. node.findPosX = function()
  2323. {
  2324. var x = 0;
  2325. var node = this;
  2326. while (node.offsetParent != null) {
  2327. x += node.offsetLeft;
  2328. node = node.offsetParent;
  2329. }
  2330. if (node.x != null)
  2331. x += node.x;
  2332. return x;
  2333. }
  2334.  
  2335. /** Calculate the absolute Y position of this HTML element.
  2336. */
  2337. node.findPosY = function()
  2338. {
  2339. var y = 0;
  2340. var node = this;
  2341. while (node.offsetParent != null) {
  2342. y += node.offsetTop;
  2343. node = node.offsetParent;
  2344. }
  2345. if (node.y != null)
  2346. y += node.y;
  2347. return y;
  2348. }
  2349.  
  2350. return node;
  2351. }
  2352.  
  2353.  
  2354. // ==================== TabSet object ====================
  2355.  
  2356. var activeTabsets = new Array();
  2357.  
  2358. // assumes that doc has already been extended
  2359. function TabSet(doc, tabsetId, tabLabels)
  2360. {
  2361. this.doc = doc;
  2362. this.tabsetId = tabsetId;
  2363. this.tabLinkMap = new Array();
  2364. this.tabDivMap = new Array();
  2365.  
  2366. // save TabSet object reference for callbacks
  2367. activeTabsets[tabsetId] = this;
  2368.  
  2369. this.getTabContent_div = function(labelText) {
  2370. return this.tabDivMap[labelText];
  2371. }
  2372.  
  2373. this.createTab = function(idx, labelText)
  2374. {
  2375. var a = this.doc.createXElement("a", {
  2376. name: this.tabsetId,
  2377. textContent: labelText,
  2378. className: "DialogBox_clickable"
  2379. });
  2380. with (a.style) {
  2381. padding = "3px 4px";
  2382. border = "1px solid Black";
  2383. MozBorderRadius = "4px";
  2384. borderBottom = "none";
  2385. fontSize = "9pt";
  2386. color = "Black";
  2387. textDecoration = "none";
  2388. }
  2389. return a;
  2390. }
  2391.  
  2392. this.activateTab = function(a) {
  2393. with (a.style) {
  2394. paddingTop = "4px";
  2395. backgroundColor = "LightGray";
  2396. }
  2397. var content_div = this.getTabContent_div(a.textContent);
  2398. content_div.show();
  2399. }
  2400.  
  2401. this.deactivateTab = function(a) {
  2402. with (a.style) {
  2403. paddingTop = "3px";
  2404. backgroundColor = "DarkGray";
  2405. }
  2406. var content_div = this.getTabContent_div(a.textContent);
  2407. content_div.hide();
  2408. }
  2409.  
  2410. this.selectTab = function(selected_a)
  2411. {
  2412. // (can be called from outside this object's context, (ie, click listener))
  2413. var tabset = activeTabsets[selected_a.name];
  2414. // deselect all tabs
  2415. tabset.doc.foreachNode("//a[@name='" + selected_a.name + "']", function(a) {
  2416. tabset.deactivateTab(a);
  2417. });
  2418. // then select the clicked tab
  2419. tabset.activateTab(selected_a);
  2420. }
  2421.  
  2422. this.initialize = function(labelText)
  2423. {
  2424. var maxX = 0;
  2425. var maxY = 0;
  2426. // determine largest width/height across content divs
  2427. for (var d in this.tabDivMap) {
  2428. var div = this.tabDivMap[d];
  2429. if (div.clientWidth > maxX) maxX = div.clientWidth;
  2430. if (div.clientHeight > maxY) maxY = div.clientHeight;
  2431. }
  2432. // equalize size of content divs to largest
  2433. for (var d in this.tabDivMap) {
  2434. var div = this.tabDivMap[d];
  2435. div.style.width = maxX;
  2436. div.style.height = maxY;
  2437. }
  2438. // select the default tab
  2439. if (labelText == null) {
  2440. labelText = tabLabels[0];
  2441. }
  2442. this.selectTab(this.tabLinkMap[labelText])
  2443. }
  2444.  
  2445.  
  2446. this.container_div = this.doc.createXElement("div", { id: this.tabsetId });
  2447.  
  2448. var ul = this.doc.createXElement("ul");
  2449. this.container_div.appendChild(ul);
  2450. with (ul.style) {
  2451. margin = "13px 7px 1px 12px";
  2452. padding = "0px 0px 0px 0px";
  2453. fontSize = "10pt";
  2454. }
  2455.  
  2456. for (var t in tabLabels)
  2457. {
  2458. var tab_a = this.createTab(t, tabLabels[t]);
  2459. tab_a.addEventListener('click', function(event) {
  2460. // now we're in the isolated context of the click
  2461. // ie, context inferred from event & globals
  2462. var selected_a = event.target;
  2463. var tabset = activeTabsets[selected_a.name];
  2464. tabset.selectTab(selected_a);
  2465. },
  2466. false
  2467. );
  2468. ul.appendChild(tab_a);
  2469. // maintatin default mouse cursor over the topic label text
  2470. tab_a.wrapIn("label");
  2471. this.tabLinkMap[tabLabels[t]] = tab_a;
  2472. // corresponding content div
  2473. var tabContent_div = this.doc.createXElement("div", {
  2474. id: this.tabsetId + ":" + tabLabels[t]
  2475. });
  2476. with (tabContent_div.style) {
  2477. margin = "0px 7px 0px 7px";
  2478. padding = "4px 4px 4px 4px";
  2479. border = "2px outset Black";
  2480. }
  2481. this.container_div.appendChild(tabContent_div);
  2482. this.tabDivMap[tabLabels[t]] = tabContent_div;
  2483. }
  2484. }
  2485.  
  2486.  
  2487. // ==================== DialogBox object ====================
  2488.  
  2489. var activeDialogs = new Array();
  2490.  
  2491. // assumes that doc has already been extended
  2492. function DialogBox(doc, dialogTitle)
  2493. {
  2494. this.doc = doc;
  2495. this.callbacks = null;
  2496.  
  2497. this.createDialog = function(popupName, dialogStyle, buttonDefs)
  2498. {
  2499. this.popupId = popupName + "_dialog";
  2500.  
  2501. var main_div = this.doc.createXElement("div");
  2502. with (main_div) {
  2503. id = this.popupId;
  2504. setAttribute("style", dialogStyle);
  2505. style.maxWidth = window.innerWidth - 50;
  2506. style.maxHeight = window.innerHeight - 70;
  2507. style.overflow = "auto";
  2508. if (style.backgroundColor == "")
  2509. style.backgroundColor = "White";
  2510.  
  2511. // dialog box structure
  2512. innerHTML =
  2513. // border layers
  2514. '<div style="border: 1px solid; border-color: Gainsboro DarkSlateGray DarkSlateGray Gainsboro;">'
  2515. + '<div style="border: 1px solid; border-color: White DarkGray DarkGray White;">'
  2516. + '<div style="border: 2px solid Gainsboro;">'
  2517. // grid (has to be a table to acheive float behaviors)
  2518. + '<table cellspacing="0" cellpadding="0">'
  2519. + '<tbody>'
  2520. // titlebar (optional)
  2521. + ((dialogTitle != null) ?
  2522. '<tr id="' + this.popupId + '_titlebar"><td'
  2523. + ' style="padding: 2px; background-color: Navy; color: White; font: bold 9pt Arial;"'
  2524. + '>' + dialogTitle
  2525. + '</td></tr>'
  2526. : "")
  2527. // main content area
  2528. + '<tr id="' + this.popupId + '_main" style="overflow: auto;"><td>'
  2529. + '<div id="' + this.popupId + '_content"/>'
  2530. + '</td></tr>'
  2531. // button bar
  2532. + '<tr id="' + this.popupId + '_buttons"><td style="padding: 6px;">'
  2533. + '</td></tr>'
  2534. + '</tbody>'
  2535. + '</table>'
  2536. + '</div>'
  2537. + '</div>'
  2538. + '</div>'
  2539. ;
  2540. }
  2541. this.doc.body.appendChild(main_div);
  2542. // $(main_div).addClass("ui-widget-content ui-draggable");
  2543. // $(main_div).draggable();
  2544.  
  2545. this.main_td = main_div.selectNodeNullable("//tr[@id='" + this.popupId + "_main']/td")
  2546. var content_div = main_div.selectNode("//div[@id='" + this.popupId + "_content']");
  2547. var buttonbar_td = main_div.selectNodeNullable("//tr[@id='" + this.popupId + "_buttons']/td")
  2548.  
  2549. var controlButtons_span = this.doc.createXElement("center");
  2550.  
  2551. if (buttonDefs != null)
  2552. {
  2553. this.callbacks = buttonDefs;
  2554. for (var b in buttonDefs)
  2555. {
  2556. var button = null;
  2557. if (b == "X")
  2558. {
  2559. var titlebar_td = main_div.selectNodeNullable("//tr[@id='" + this.popupId + "_titlebar']/td")
  2560. if (titlebar_td != null) {
  2561. // X close button in the right side of the titlebar
  2562. button = this.doc.createXElement("a");
  2563. with (button) {
  2564. id = this.popupId + "_closer";
  2565. href = "javascript:void(0)";
  2566. with (style) {
  2567. cssFloat = "right";
  2568. border = "1px solid";
  2569. borderColor = "White DarkSlateGray DarkSlateGray White";
  2570. backgroundColor = "LightGray";
  2571. padding = "0px 1px 0px 2px";
  2572. font = "bold 9pt Arial";
  2573. color = "Black";
  2574. textAlign = "center";
  2575. lineHeight = "110%";
  2576. }
  2577. appendChildText("X");
  2578. }
  2579. titlebar_td.prependChild(button);
  2580. }
  2581. else {
  2582. // X close button in the upper-right of window
  2583. button = this.doc.createXElement("a");
  2584. with (button) {
  2585. id = this.popupId + "_closer";
  2586. href = "javascript:void(0)";
  2587. with (style) {
  2588. cssFloat = "right";
  2589. backgroundColor = "#AA0000";
  2590. padding = "2px";
  2591. font = "bold 8pt Arial";
  2592. textDecoration = "none";
  2593. color = "White";
  2594. }
  2595. appendChildText("X");
  2596. }
  2597. content_div.prependSibling(button);
  2598. }
  2599. }
  2600. else {
  2601. // a regular button at bottom of window
  2602. button = this.doc.createXElement("button");
  2603. with (button.style) {
  2604. margin = "0px 5px";
  2605. fontSize = "8pt";
  2606. fontFamily = "Helvetica, sans-serif";
  2607. }
  2608. controlButtons_span.appendChild(button);
  2609. }
  2610.  
  2611. with (button) {
  2612. name = this.popupId; // name attr associates callbacks with the dialog id
  2613. className = "DialogBox_clickable";
  2614. textContent = b;
  2615. addEventListener('click', function(event) {
  2616. // now we're in the isolated context of the click
  2617. // ie, context inferred from event & globals
  2618. var doc = extendDocument(event.target.ownerDocument);
  2619. var dialog = activeDialogs[event.target.name];
  2620. var popupId = event.target.textContent;
  2621. var callbackFunc = dialog.callbacks[popupId];
  2622. dialog.hidePopup();
  2623. callbackFunc(doc);
  2624. dialog.removePopup();
  2625. },
  2626. false
  2627. );
  2628. }
  2629. }
  2630. buttonbar_td.appendChild(controlButtons_span);
  2631.  
  2632. this.doc.addStyle(
  2633. ".DialogBox_clickable:hover { cursor: pointer; }\n"
  2634. );
  2635. }
  2636.  
  2637. // save DialogBox object reference for callbacks
  2638. activeDialogs[this.popupId] = this;
  2639.  
  2640. return content_div;
  2641. }
  2642.  
  2643. this.hidePopup = function() {
  2644. var div = this.doc.getElementById(this.popupId);
  2645. div.style.display = "none";
  2646. }
  2647.  
  2648. this.removePopup = function() {
  2649. var div = this.doc.getElementById(this.popupId);
  2650. div.parentNode.removeChild(div);
  2651.  
  2652. activeDialogs[this.popupId] = null;
  2653. }
  2654. }
  2655.  
  2656. function noop() {
  2657. }
  2658.  
  2659.  
  2660. // ==================== Preferences object ====================
  2661.  
  2662. /** (This object is created before the Logger object,
  2663. * therefore the log methods cannot be used. Use GM_log instead.)
  2664. */
  2665. function Preferences(defaultValuesMap)
  2666. {
  2667. this.defaultValuesMap = defaultValuesMap;
  2668. this.cacheMap = new Object();
  2669.  
  2670. /** Adds additional attributes to the map.
  2671. */
  2672. // TBD: rename (add, merge)
  2673. this.config = function(valuesMap) {
  2674. for (var k in valuesMap) {
  2675. this.defaultValuesMap[k] = valuesMap[k];
  2676. }
  2677. }
  2678.  
  2679. this.get = function(prefName)
  2680. {
  2681. var value = this.cacheMap[prefName];
  2682. if (typeof(value) == "undefined")
  2683. {
  2684. value = GM_getValue(prefName);
  2685. if (typeof(value) == "undefined")
  2686. {
  2687. value = this.defaultValuesMap[prefName];
  2688. if (typeof(value) == "undefined") {
  2689. GM_log("Unmanaged preference: " + prefName);
  2690. return value;
  2691. }
  2692. }
  2693. this.set(prefName, value);
  2694. }
  2695. return value;
  2696. }
  2697.  
  2698. this.set = function(prefName, prefValue)
  2699. {
  2700. GM_setValue(prefName, prefValue);
  2701. this.cacheMap[prefName] = prefValue;
  2702. }
  2703.  
  2704. this.getAsList = function(prefName, delim, wrapperType)
  2705. {
  2706. var value = this.get(prefName);
  2707. var valueList;
  2708. if (value != null) {
  2709. valueList = value.split(delim);
  2710. }
  2711. else {
  2712. valueList = new Array();
  2713. }
  2714.  
  2715. if (wrapperType != null) {
  2716. // wrap elements in custom object type
  2717. var wrappedValueList = new Array();
  2718. for (var i=0; i < valueList.length; i++) {
  2719. wrappedValueList[i] = new wrapperType(valueList[i]);
  2720. }
  2721. return wrappedValueList;
  2722. }
  2723.  
  2724. // add utility methods to the resulting Array object
  2725.  
  2726. valueList.contains = function(matchText)
  2727. {
  2728. if (matchText == null) {
  2729. log.error("a null arg: " + this + " " + matchText);
  2730. return false;
  2731. }
  2732.  
  2733. for (var i in this) {
  2734. if (matchText == this[i])
  2735. return true;
  2736. }
  2737. return false;
  2738. }
  2739.  
  2740. return valueList;
  2741. }
  2742. }
  2743.  
  2744.  
  2745. // ==================== PreferencesManager object ====================
  2746.  
  2747. function setScreenPosition(node, posIndicator)
  2748. {
  2749. with (node.style)
  2750. {
  2751. position = "fixed";
  2752. zIndex = 999;
  2753. switch (posIndicator) {
  2754. case "TL": top = 0; left = 0; break;
  2755. case "TR": top = 0; right = 0; break;
  2756. case "BL": bottom = 0; left = 0; break;
  2757. case "BR": bottom = 0; right = 0; break;
  2758. default:
  2759. log.error("Unrecognized menu position indicator: " + menuPos);
  2760. }
  2761. }
  2762. }
  2763.  
  2764. function PreferencesManager(doc, uniqId, title, buttonDefs)
  2765. {
  2766. this.doc = extendDocument(doc);
  2767. this.uniqId = uniqId;
  2768. this.dialogBox = new DialogBox(this.doc, title);
  2769. this.buttonDefs = buttonDefs;
  2770.  
  2771. /** Display the Preferences dialog.
  2772. */
  2773. this.open = function()
  2774. {
  2775. if (this.doc.selectNodeNullable("//div[@id='" + this.uniqId + "_dialog']")) {
  2776. log.info("Preferences dialog already open");
  2777. return null; // the dialog is already open
  2778. }
  2779.  
  2780. var dialogBox_div = this.dialogBox.createDialog(
  2781. this.uniqId,
  2782. "z-index: 999; left: 15%; top: 25px; position: fixed;"
  2783. + " background-color: LightGray;",
  2784. this.buttonDefs
  2785. );
  2786. with (dialogBox_div.style) {
  2787. fontSize = "10pt";
  2788. fontFamily = "Arial, Helvetica, sans-serif";
  2789. overflow = "auto";
  2790. backgroundColor = "LightGray";
  2791. }
  2792.  
  2793. return dialogBox_div;
  2794. }
  2795.  
  2796. /** Create an HTML input element associated with the named greasemonkey preference.
  2797. */
  2798. this.createPreferenceInput = function(prefName, titleText, tipText, attrMap, optionMap)
  2799. {
  2800. var prefValue = prefs.get(prefName);
  2801. var item_label;
  2802. var inputTagname = "input";
  2803. switch (typeof(prefValue)) {
  2804. case "boolean":
  2805. item_label = this.doc.createCheckbox(titleText, attrMap, prefValue);
  2806. break;
  2807. case "string":
  2808. case "number":
  2809. if (optionMap != null) {
  2810. item_label = this.doc.createSelect(titleText, attrMap, optionMap, prefValue);
  2811. inputTagname = "select";
  2812. }
  2813. else if (attrMap["rows"] != null) {
  2814. item_label = this.doc.createTextArea(titleText, attrMap, prefValue);
  2815. inputTagname = "textarea";
  2816. }
  2817. else {
  2818. item_label = this.doc.createInputText(titleText, attrMap, prefValue);
  2819. }
  2820. break;
  2821. default:
  2822. log.warn("For " + prefName + ", unrecognized type: " + typeof(prefValue));
  2823. }
  2824. item_label.style.fontSize = "9pt";
  2825. if (tipText != null)
  2826. item_label.title = tipText;
  2827. with (item_label.selectNode(inputTagname)) {
  2828. name = prefName;
  2829. className = "preferenceSetting";
  2830. applyAttributes(attrMap);
  2831. }
  2832. return item_label;
  2833. }
  2834.  
  2835. this.createScreenCornerPreference = function(prefName)
  2836. {
  2837. var prefValue = prefs.get(prefName);
  2838.  
  2839. var table = this.doc.createXElement("table", {
  2840. id: prefName + "_2x2"
  2841. });
  2842. with (table) {
  2843. style.borderCollapse = "collapse";
  2844. cellPadding = 0; cellSpacing = 0;
  2845.  
  2846. appendTableRow([ createRadioButton("TL"), null, createRadioButton("TR") ]);
  2847. appendTableRow([ null, null, null ]);
  2848. appendTableRow([ createRadioButton("BL"), null, createRadioButton("BR") ]);
  2849.  
  2850. style.border = "3px inset Black";
  2851. foreachNode(".//input", function(inp) {
  2852. inp.style.margin = "0px";
  2853. });
  2854. with (selectNode(".//tr[2]/td[2]")) {
  2855. // acheive roughly 4/3 aspect ratio
  2856. style.width = "14px";
  2857. style.height = "4px";
  2858. };
  2859. }
  2860. return table;
  2861.  
  2862. function createRadioButton(choiceValue)
  2863. {
  2864. var radio_input = doc.createXElement("input", {
  2865. type: "radio", name: prefName, value: choiceValue,
  2866. className: "preferenceSetting"
  2867. });
  2868. if (choiceValue == prefValue) {
  2869. radio_input.checked = true;
  2870. }
  2871. return radio_input;
  2872. }
  2873. }
  2874.  
  2875. /** Store current screen values into the associated Preferences,
  2876. * but only for values that have changed.
  2877. * (This is the primary logic for the OK button)
  2878. */
  2879. this.storePrefs = function()
  2880. {
  2881. this.doc.foreachNode("//*[@class='preferenceSetting']", function(inputObj) {
  2882. var prefName = inputObj.name;
  2883. var prefValue;
  2884. if (inputObj.type == "checkbox") {
  2885. prefValue = inputObj.checked;
  2886. }
  2887. else if (inputObj.type == "radio") {
  2888. if (inputObj.checked)
  2889. prefValue = inputObj.value;
  2890. else
  2891. return; // skip all in group except the checked one
  2892. }
  2893. else {
  2894. prefValue = inputObj.value;
  2895. }
  2896.  
  2897. var oldValue = GM_getValue(prefName, prefValue);
  2898. if (prefValue != oldValue)
  2899. {
  2900. var defaultValue = prefs.get(prefName);
  2901. if (typeof(defaultValue) == "number") {
  2902. if (isNaN(prefValue)) {
  2903. alert("Non-numeric value '" + prefValue + "' is invalid for preference " + prefName);
  2904. return false; // continue on to next preference item
  2905. }
  2906. prefValue = parseFloat(prefValue);
  2907. }
  2908. if (typeof(prefValue) == "string")
  2909. log.info("Setting preference: " + prefName + " => '" + prefValue + "'");
  2910. else
  2911. log.info("Setting preference: " + prefName + " => " + prefValue);
  2912. prefs.set(prefName, prefValue);
  2913. }
  2914. });
  2915. }
  2916. }
  2917.  
  2918.  
  2919. // ==================== Collapsible object ====================
  2920.  
  2921. function Collapsible(theNode, collapserId, isPersistent, isInitExpanded)
  2922. {
  2923. this.node = theNode;
  2924. this.doc = extendDocument(theNode.ownerDocument);
  2925.  
  2926. if (collapserId == null) {
  2927. if (theNode.id == null)
  2928. collapserId = "collapser_" + generateUuid();
  2929. else
  2930. collapserId = theNode.id + "_collapser";
  2931. }
  2932.  
  2933. // maintain object reference(s) for callbacks
  2934. if (document.activeCollapsers == null) {
  2935. document.activeCollapsers = new Object();
  2936. }
  2937. document.activeCollapsers[collapserId] = this;
  2938.  
  2939. this.expand = function(event) {
  2940. collapsible = this;
  2941. if (event != null) {
  2942. var collapserId = event.target.parentNode.id;
  2943. collapsible = document.activeCollapsers[collapserId];
  2944. if (isPersistent) {
  2945. prefs.set(collapserId, true);
  2946. }
  2947. }
  2948. collapsible.node.show();
  2949. collapsible.expander.hide();
  2950. collapsible.collapser.show();
  2951. }
  2952.  
  2953. this.collapse = function(event) {
  2954. var collapsible = this;
  2955. if (event != null) {
  2956. var collapserId = event.target.parentNode.id;
  2957. collapsible = document.activeCollapsers[collapserId];
  2958. if (isPersistent) {
  2959. prefs.set(collapserId, false);
  2960. }
  2961. }
  2962. collapsible.node.hide();
  2963. collapsible.collapser.hide();
  2964. collapsible.expander.show();
  2965. }
  2966.  
  2967. this.createController = function(func, base64) {
  2968. var img = this.doc.createXElement("img");
  2969. // img.title = label;
  2970. img.src = 'data:image/gif;base64,' + base64;
  2971. img.addEventListener('click', func, false);
  2972.  
  2973. with (img.style) {
  2974. cssFloat = "left";
  2975. left = "0px";
  2976. position = "absolute";
  2977. zIndex = 999;
  2978. }
  2979. return img;
  2980. }
  2981.  
  2982. var span = this.doc.createXElement("span", { id: collapserId });
  2983. this.node.prependSibling(span);
  2984.  
  2985. this.expander = this.createController(this.expand,
  2986. 'R0lGODlhEAAQAKEDAAAA/wAAAMzMzP///yH5BAEAAAMALAAAAAAQABAAAAIhnI+pywOtwINHTmpvy3rx' +
  2987. 'nnABlAUCKZkYoGItJZzUTCMFACH+H09wdGltaXplZCBieSBVbGVhZCBTbWFydFNhdmVyIQAAOw=='
  2988. );
  2989. span.appendChild(this.expander);
  2990.  
  2991. this.collapser = this.createController(this.collapse,
  2992. 'R0lGODlhEAAQAKEDAAAA/wAAAMzMzP///yH5BAEAAAMALAAAAAAQABAAAAIdnI+py+0Popwx0RmEuiAz' +
  2993. '6jVS6HTaY5zoyrZuWwAAIf4fT3B0aW1pemVkIGJ5IFVsZWFkIFNtYXJ0U2F2ZXIhAAA7'
  2994. );
  2995. span.appendChild(this.collapser);
  2996.  
  2997. var isExpanded = isInitExpanded;
  2998. if (isPersistent == true) {
  2999. isExpanded = prefs.get(collapserId);
  3000. }
  3001.  
  3002. if (isExpanded)
  3003. this.expand()
  3004. else
  3005. this.collapse()
  3006. }
  3007.  
  3008.  
  3009. // ==================== DocumentContainer object ====================
  3010.  
  3011. /** Create and manage invisible iframe content loaded from an arbitrary URL.
  3012. * If the same URL is requested more than once, it is returned from cache.
  3013. * Example:
  3014. * var dc = new DocumentContainer();
  3015. * dc.loadFromSameOrigin("search.do?category=eligible",
  3016. * function(doc) {
  3017. * if (dm.xdoc.selectNode("//text()[.='Dilbert']"))
  3018. * alert("Hide your daughters!");
  3019. * }
  3020. * );
  3021. */
  3022. function DocumentContainer(debugFlag)
  3023. {
  3024. var iframeCache = new Array();
  3025. this.debug = debugFlag;
  3026.  
  3027. this.loadFromSameOrigin = function(theUrl, theFunc)
  3028. {
  3029. var iframe = iframeCache[theUrl];
  3030. if (iframe != null) {
  3031. if (theFunc != null)
  3032. theFunc(iframe.contentDocument);
  3033. return;
  3034. }
  3035.  
  3036. var iframe = this.attachIframe(theUrl);
  3037.  
  3038. // wait for the DOM to be available, then dispatch
  3039. iframe.addEventListener(
  3040. "load",
  3041. function(evt) {
  3042. var theIframe = evt.currentTarget;
  3043. var theUrl = theIframe.contentWindow.location.href;
  3044. iframeCache[theUrl] = theIframe;
  3045. if (theFunc != null)
  3046. theFunc(theIframe.contentDocument);
  3047. },
  3048. false
  3049. );
  3050.  
  3051. // load the content
  3052. iframe.contentWindow.location.href = ajaxstaticUrl(theUrl);
  3053. }
  3054.  
  3055. this.loadFromForeignOrigin = function(theUrl, theFunc)
  3056. {
  3057. if (window != top) {
  3058. return; // prevent infinite recursion
  3059. }
  3060. var iframe = this.attachIframe(theUrl);
  3061.  
  3062. GM_xmlhttpRequest(
  3063. {
  3064. method: "GET",
  3065. url: ajaxstaticUrl(theUrl),
  3066. onload: function(details) {
  3067.  
  3068. // give it a URL so that it will create a .contentDocument property.
  3069. // Make it the same as the current page,
  3070. // Otherwise, same-origin policy would prevent us.
  3071. // TBD: why is this a literal? Would foobar.com work as well??
  3072. iframe.contentWindow.location.href = "http://tv.yahoo.com/";
  3073.  
  3074. // wait for the DOM to be available, then dispatch
  3075. iframe.addEventListener(
  3076. "DOMContentLoaded",
  3077. function() {
  3078. if (theFunc != null)
  3079. theFunc(iframe.contentDocument);
  3080. },
  3081. false
  3082. );
  3083.  
  3084. // write the received content into the document
  3085. iframe.contentDocument.open("text/html");
  3086. iframe.contentDocument.write(details.responseText);
  3087. iframe.contentDocument.close();
  3088. }
  3089. });
  3090.  
  3091. return iframe.contentDocument;
  3092. }
  3093.  
  3094. this.attachIframe = function(theUrl)
  3095. {
  3096. // create an IFRAME element to write the document into.
  3097. // It must be added to the document and rendered (eg, display != none)
  3098. // to be properly initialized.
  3099. var iframe = document.createElement("iframe");
  3100. iframe.id = "DocumentContainer_" + theUrl;
  3101. if (this.debug == null) {
  3102. iframe.width = 0;
  3103. iframe.height = 0;
  3104. iframe.style.display = "none";
  3105. }
  3106. else {
  3107. iframe.width = 800;
  3108. iframe.height = 700;
  3109. }
  3110. document.body.appendChild(iframe);
  3111.  
  3112. iframe.contentWindow.location.href = "about:blank";
  3113.  
  3114. return iframe;
  3115. }
  3116.  
  3117. // private helper methods
  3118. }
  3119.  
  3120. /** Add param to URL, marking it as not to be re-processed.
  3121. */
  3122. function ajaxstaticUrl(theUrl)
  3123. {
  3124. var newUrl = theUrl;
  3125. if (newUrl.indexOf("?") == -1)
  3126. newUrl += "?";
  3127. if (newUrl.indexOf("?") != newUrl.length-1)
  3128. newUrl += "&";
  3129. return newUrl + "ajaxstatic";
  3130. }
  3131.  
  3132. /** Retrieve each document specified in the urlList
  3133. * invoking onloadFunc with each doc,
  3134. * and then finally invoking onrendezvousFunc with the assembled list of docs
  3135. */
  3136. function withDocuments(urlList, onloadFunc, onrendezvousFunc)
  3137. {
  3138. var context = new Object();
  3139. context.resultDocList = new Array();
  3140. context.pendingCount = urlList.length;
  3141.  
  3142. for (var u in urlList)
  3143. {
  3144. var dc = new DocumentContainer();
  3145. dc.loadFromSameOrigin(urlList[u],
  3146. function(curDoc) {
  3147. if (onloadFunc != null) {
  3148. onloadFunc(curDoc);
  3149. }
  3150. if (--context.pendingCount == 0) {
  3151. if (onrendezvousFunc != null) {
  3152. context.resultDocList.push(curDoc);
  3153. onrendezvousFunc(context.resultDocList);
  3154. }
  3155. }
  3156. }
  3157. );
  3158. }
  3159. }
  3160.  
  3161. /** Recursively retrieve each document specified in the urlList,
  3162. * then invoke the dispatch function with the list of loaded docs.
  3163. */
  3164. function withDocumentsSerialized(urlList, func, docList)
  3165. {
  3166. var curUrl = urlList.shift();
  3167. if (docList == null)
  3168. docList = new Array();
  3169.  
  3170. var dc = new DocumentContainer();
  3171. dc.loadFromSameOrigin(curUrl,
  3172. function(curDoc) {
  3173. if (urlList.length > 0)
  3174. withDocuments(urlList, func, docList);
  3175. else
  3176. func(docList);
  3177. }
  3178. );
  3179. }
  3180.  
  3181.  
  3182.  
  3183. // ==================== Logger object ====================
  3184.  
  3185. function Logger(verNum)
  3186. {
  3187. this.logLevels = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
  3188.  
  3189. this.level = null;
  3190.  
  3191. this.setLevel = function(level) {
  3192. this.level = level;
  3193. if (level >= 2)
  3194. GM_log("[" + verNum + "] === LOGGER LEVEL: " + this.logLevels[this.level] + " ==="
  3195. + " " + document.location.href);
  3196. }
  3197.  
  3198. this.setLevel(arrayIndexOf(this.logLevels, prefs.get("loggerLevel")));
  3199.  
  3200. this.error = function(msg) { if (this.level >= 0) GM_log("ERROR: " + msg); }
  3201. this.warn = function(msg) { if (this.level >= 1) GM_log("WARN: " + msg); }
  3202. this.info = function(msg) { if (this.level >= 2) GM_log("INFO: " + msg); }
  3203. this.debug = function(msg) { if (this.level >= 3) GM_log("DEBUG: " + msg); }
  3204. this.trace = function(msg) { if (this.level >= 4) GM_log("TRACE: " + msg); }
  3205.  
  3206. this.getLogLevelMap = function() { return IdentityMapForArray(this.logLevels); };
  3207. }
  3208.  
  3209.  
  3210. // ==================== JavaScript object extenstions ====================
  3211.  
  3212. function extendJavascriptObjects()
  3213. {
  3214. // ---------- String extensions ----------
  3215.  
  3216. /** Format text content as it will appear on a page (before wrapping, etc).
  3217. */
  3218. String.prototype.normalizeWhitespace = function()
  3219. {
  3220. var text = this.replace(/\s+/g, " "); // reduce internal whitespace
  3221. text = text.replace(/ ([,;:\.!])/g, "$1"); // snug-up punctuation
  3222. return text.trimWhitespace();
  3223. }
  3224.  
  3225. /** Format text content as it will appear on a page (before wrapping, etc).
  3226. */
  3227. String.prototype.trimWhitespace = function()
  3228. {
  3229. return this.replace(/^\s*/, "").replace(/\s*$/, "");
  3230. }
  3231.  
  3232. String.prototype.stripQuoteMarks = function()
  3233. {
  3234. var text = this.replace(/"/g, "");
  3235. return text;
  3236. }
  3237.  
  3238. // ---------- Date extensions ----------
  3239.  
  3240. SECOND = 1000;
  3241. MINUTE = SECOND * 60;
  3242. HOUR = MINUTE * 60;
  3243. DAY = HOUR * 24;
  3244. WEEK = DAY * 7;
  3245.  
  3246. // Example, on the hour: floor(Date.HOUR)
  3247. Date.prototype.floor = function(unit) {
  3248. var floorMilli = Math.floor(this.getTime() / unit) * unit;
  3249. return new Date(floorMilli);
  3250. }
  3251.  
  3252. Date.prototype.add = function(millis) {
  3253. return new Date(this.getTime() + millis);
  3254. }
  3255. }
  3256.  
  3257.  
  3258. // ---------- Array helpers ----------
  3259.  
  3260. function arrayIndexOf(theList, value, attrName)
  3261. {
  3262. if (attrName == null) {
  3263. // by element value
  3264. for (var i in theList) {
  3265. if (theList[i] == value)
  3266. return i;
  3267. }
  3268. }
  3269. else {
  3270. if (typeof(value) == "object") {
  3271. // by corresponding attribute in value array
  3272. for (var i in theList) {
  3273. if (theList[i][attrName] == value[attrName])
  3274. return i;
  3275. }
  3276. }
  3277. else {
  3278. // by attribute value
  3279. for (var i in theList) {
  3280. if (theList[i][attrName] == value) {
  3281. return i;
  3282. }
  3283. }
  3284. }
  3285. }
  3286. return null;
  3287. }
  3288.  
  3289. function sortBy(theList, fieldList)
  3290. {
  3291. theList.sort( function(a, b)
  3292. {
  3293. for (var i in fieldList) {
  3294. if (a[fieldList[i]] < b[fieldList[i]]) return -1;
  3295. if (a[fieldList[i]] > b[fieldList[i]]) return 1;
  3296. }
  3297. return 0;
  3298. });
  3299. return theList;
  3300. }
  3301.  
  3302. function sortDescBy(theList, fieldList)
  3303. {
  3304. theList.sort( function(a, b)
  3305. {
  3306. for (var i in fieldList) {
  3307. if (a[fieldList[i]] > b[fieldList[i]]) return -1;
  3308. if (a[fieldList[i]] < b[fieldList[i]]) return 1;
  3309. }
  3310. return 0;
  3311. });
  3312. return theList;
  3313. }
  3314.  
  3315. function numericComparatorAsc(a, b) {
  3316. return (a-b);
  3317. }
  3318.  
  3319. function numericComparatorDesc(a, b) {
  3320. return (b-a);
  3321. }
  3322.  
  3323. /** .
  3324. */
  3325. function IdentityMapForArray(ary)
  3326. {
  3327. var map = new Array();
  3328. for (var i=0; i < ary.length; i++) {
  3329. map[ary[i]] = ary[i];
  3330. }
  3331. return map;
  3332. }
  3333.  
  3334. /** Create a new Array with pre-defined numeric indices,
  3335. * (ie, ready for inserts to random indices).
  3336. */
  3337. function initArrayIndices(count) {
  3338. var a = new Array(count);
  3339. for (var i = 0; i < count; i++) {
  3340. a[i] = null;
  3341. }
  3342. return a;
  3343. }
  3344.  
  3345.  
  3346. /** Dispatch processing for each grouping of elements based upon the named field.
  3347. * Example:
  3348. * var nodes = dm.xdoc.selectNodes("//*[@class]");
  3349. * GM_log(nodes.length + " nodes");
  3350. * foreachGrouping(sortBy(nodes, ["className"] ), "className", function(groups) {
  3351. * GM_log(groups.length + " nodes with class='" + groups[0].className+ "'");
  3352. * });
  3353. */
  3354. function foreachGrouping(theList, attrName, func)
  3355. {
  3356. var curList = new Array();
  3357. var prevValue = null;
  3358. for (var i in theList)
  3359. {
  3360. if (theList[i][attrName] != prevValue)
  3361. {
  3362. if (curList.length > 0) {
  3363. func(curList);
  3364. }
  3365. curList = new Array();
  3366. }
  3367. curList.push(theList[i]);
  3368. prevValue = theList[i][attrName];
  3369. }
  3370. }
  3371.  
  3372.  
  3373. // ==================== UrlParser object ====================
  3374.  
  3375. /** Parsing and formatting of URLs.
  3376. * url, params; scheme, host, port, path
  3377. */
  3378. function UrlParser(urlString)
  3379. {
  3380. var urlParts = urlString.split("?");
  3381. this.url = urlParts[0];
  3382. this.parms = new Array();
  3383.  
  3384. // parse query params into name/value associative list
  3385. if (urlParts[1]) {
  3386. var queryItems = urlParts[1].split("&");
  3387.  
  3388. for (var i in queryItems) {
  3389. var parm = queryItems[i].split("=");
  3390. this.parms[unescape(parm[0])] = unescape(parm[1]);
  3391. // convert to numeric if appropriate
  3392. var num = parseInt(parm[1]);
  3393. if (!isNaN(num) && parm[1].substring(0, 1) != "0") {
  3394. this.parms[unescape(parm[0])] = num;
  3395. }
  3396. }
  3397. }
  3398.  
  3399. // parse http://domain/path into scheme, domain, path
  3400. this.url.match(/(\w+):\/\/([\w\.]+)(\/.*)/);
  3401. this.scheme = RegExp.$1;
  3402. this.host = RegExp.$2;
  3403. this.path = RegExp.$3;
  3404.  
  3405. // METHODS
  3406.  
  3407. // assemble the query part of the URL
  3408. this.getQuery = function()
  3409. {
  3410. queryItems = new Array();
  3411. for (var p in this.parms) {
  3412. if (this.parms[p])
  3413. queryItems.push(escape(p) + "=" + escape(this.parms[p]));
  3414. }
  3415. if (queryItems.length == 0) {
  3416. return "";
  3417. }
  3418. else {
  3419. return "?" + queryItems.join("&");
  3420. }
  3421. }
  3422.  
  3423. // assemble the whole URL
  3424. this.toString = function()
  3425. {
  3426. return this.url + this.getQuery();
  3427. }
  3428. }
  3429.  
  3430.  
  3431. // --------------- helper functions ---------------
  3432.  
  3433. /** Lookup preference setting and conditionally execute with error handling.
  3434. */
  3435. function dispatchFeature(feaureName, func)
  3436. {
  3437. if (prefs.get(feaureName))
  3438. {
  3439. tryCatch("feature: " + feaureName, func);
  3440. }
  3441. }
  3442.  
  3443. /** Provide debug info if function throws an exception.
  3444. */
  3445. function tryCatch(desc, func)
  3446. {
  3447. try { func(); }
  3448. catch(err) {
  3449. log.error(
  3450. "exception @ " + err.lineNumber + " [" + desc + "]" + " : " + err + "\n"
  3451. + genStackTrace(arguments.callee)
  3452. );
  3453. }
  3454. }
  3455.  
  3456. /** Generate a UUID.
  3457. */
  3458. function generateUuid() {
  3459. return (S4()+S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4()+S4()+S4());
  3460.  
  3461. function S4() {
  3462. // 5 digit random #
  3463. return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
  3464. }
  3465. }
  3466.  
  3467. // --------------- Stack Trace ---------------
  3468.  
  3469. function genStackTrace(func)
  3470. {
  3471. var depthLimit = 20;
  3472. var stackTrace = "Stack trace:\n";
  3473. while (func != null) {
  3474. if (--depthLimit < 0) {
  3475. stackTrace += "more ...\n";
  3476. break;
  3477. }
  3478. stackTrace += "called by: " + getFunctionSignature(func) + "\n";
  3479. // TBD: line# within func
  3480. func = func.caller;
  3481. }
  3482.  
  3483. return stackTrace + "\n\n";
  3484. }
  3485.  
  3486. function getFunctionSignature(func)
  3487. {
  3488. var signature = getFunctionName(func);
  3489. signature += "(";
  3490. for (var i = 0; i < func.arguments.length; i++)
  3491. {
  3492. // trim long arguments
  3493. var nextArgument = func.arguments[i];
  3494. if (nextArgument.length > 30)
  3495. nextArgument = nextArgument.substring(0, 30) + "...";
  3496.  
  3497. // apend the next argument to the signature
  3498. signature += "'" + nextArgument + "'";
  3499.  
  3500. // comma separator
  3501. if (i < func.arguments.length - 1)
  3502. signature += ", ";
  3503. }
  3504. signature += ")";
  3505.  
  3506. return signature;
  3507. }
  3508.  
  3509. function getFunctionName(func)
  3510. {
  3511. // mozilla makes it easy
  3512. if (func.name != null) {
  3513. return func.name;
  3514. }
  3515.  
  3516. // try to parse the function name from the defintion
  3517. var definition = func.toString();
  3518. var name = definition.substring(
  3519. definition.indexOf('function') + 8,
  3520. definition.indexOf('(')
  3521. );
  3522. if (name != null)
  3523. return name;
  3524.  
  3525. // sometimes there won't be a function name (eg, dynamic functions)
  3526. return "anonymous";
  3527. }