Also see Install.
1 // ==UserScript==
2 // @name LPLFixup
3 // @version 12
4 // @namespace http://freecog.net/2006/
5 // @description Fixes a variety of annoyances at the La Crosse Public Library web site.
6 // @include http://lacrosselibrary.org/*
7 // @include http://www.lacrosselibrary.org/*
8 // @include http://lacrosse.lib.wi.us/*
9 // @include http://www.lacrosse.lib.wi.us/*
10 // @include http://lpl.lacrosse.lib.wi.us/*
11 // @include http://lpl.lacrosse.lib.wi.us:81/*
12 // @include http://lplcat.lacrosse.lib.wi.us/rpa/webauth.exe*
13 // @include http://lplcat.lacrosse.lib.wi.us:8080/rpa/webauth.exe*
14 // ==/UserScript==
15
16 //GM_log("LPLFixup running.");
17
18 // Changelog
19 /*
20
21 Version 12, 10 October 2007:
22 Fixed a bug that would cause the script to break if you had no saved information.
23
24 Version 11, 18 February 2007:
25 Added support for filling of the "Remote Patron Authentication" form.
26 Minor aesthetic improvements.
27 Added fixing of the "Reader's Guide" link in the Information Toolbox tab so that
28 the user is not prompted to select a library (New pref: boolean
29 "fix_readers_guide_link")
30 Fixed a bug in the rewriting of javascript: URLs.
31
32 Version 10, 7 February 2007:
33 New options dialog; many more exposed options with extended explanations.
34 Renamed pref "fix_click_for_more_search_options_text" to "fix_more_search_options_link".
35
36 Version 9, 4 February 2007:
37 Bugfix: "www" subdomain versions of URLs for which this script is included added.
38
39 Version 8, 20 Jan 2007:
40 User preferences (e.g., the checkboxes at the bottom of the page) are now automatically
41 propagated to pages in other windows or tabs.
42
43 Version 7, 5 Jan 2007:
44 Added better support for "My List" page titles.
45 Finally got the session timeout prevention mechanism working. (New pref: boolean
46 "maintain_session"; New pref: int "maintain_session_interval"; New pref: int
47 "session_timeout_interval")
48
49 Version 6, 1 Jan 2007:
50 Added JavaScript link rewriting (New pref: boolean "fix_js_links", disabled
51 by default) This is disabled because it may cause strange behavior--
52 more testing is necessary.
53 Added fixing of the "More Search Options" link, which is by default called
54 "Click For More Search Options". (New pref: boolean
55 "fix_click_for_more_search_options_text")
56 Added fixing of how the search history page displays "More Search Options"
57 searches as "Click For;More Search Options" (New pref: boolean
58 "fix_more_search_options_history")
59
60 Version 5, 1 Jan 2007:
61 Fixed bug that attempted to parse the URL on a logout page.
62 Added support for infering the title on search form pages.
63 Added some comments.
64 Restructured the title code.
65 Added fixing of the "Reader's Guide" link under "Information Toolbox" so that it looks
66 like a normal link, instead of a visited one. (New pref: boolean
67 "fix_readers_guide_link_style")
68 New pref: boolean "fix_title" -- globabl flag for fixing of page titles
69 Added fixing of the "Local news index&Fast Facts" tab text.
70 Added the Community Resources site to the include list: http://lpl.lacrosse.lib.wi.us:81/*
71 Added support for titles for Community Resources record pages.
72 Happy new year.
73
74 Version 4, 31 Dec 2006:
75 Added fixing of the "More Search Options" page title, which is "Click Here For ;More
76 Search Options" by default. (New pref: boolean "fix_more_search_options_title")
77 Fixed typo: 'Accound Overview' -> 'Account Overview'
78 Minor styling enhancements.
79 Reused UI text moved to `ui_strings` dictionary.
80 Added fixing of the background image on pages with the flowery red image (which tiles
81 on wide monitors).
82 Removed use of UTF-8 Unicode literals in favor of escape sequences (due to a stupid
83 Greasemonkey bug).
84 Fixed a bug that caused POST pages to have incorrect titles missing the last two letters.
85
86 Version 3, 17 Dec 2006:
87 Added document title inference. (New pref: boolean "end_titles_with_lpl".)
88 Added null removal. (New pref: boolean "hide_null".)
89 Added non-link style fixer (New pref: boolean "fix_nonlink_styles".)
90
91 Version 2, 17 Dec 2006: Initial public release
92
93 version 1: Initial version
94
95 */
96
97
98 // Unicode symbol quick reference
99 /*
100 Hyphen: \u2010
101 EN-Dash: \u2013
102 EM-Dash: \u2014
103 Left single quotation mark: \u2018
104 Right single quotation mark: \u2019
105 Left double quotation mark: \u201c
106 Right double quotation mark: \u201d
107 Interrobang: \u203d // Just 'cause I can.
108 */
109
110
111 const DEBUG = GM_getValue("debug", false);
112 // Don't access the Firebug console through the unsafeWindow object?
113 const SAFE_DEBUG = GM_getValue("safe_debug", false);
114
115 function log() {
116 if (DEBUG) {
117 if (!SAFE_DEBUG && unsafeWindow.console) {
118 // It's nice to bypass Greasemonkey's logging function because
119 // that function prepends a long namespace to the logged value,
120 // wasting screen space.
121 unsafeWindow.console.log.apply(unsafeWindow.console, arguments);
122 } else {
123 GM_log.apply(null, arguments);
124 }
125 }
126 };
127
128
129 // Oh, how I long,
130 // for MochiKit dot DOM.
131 function El(name /* children... */) {
132 var a, el = document.createElement(name.match(/^[^.#$\s]+/)[0]);
133 for (var i = 1; i < arguments.length; i++) {
134 a = arguments[i];
135 if (a.__dom__) a = a.__dom__(el);
136 else if (a.dom) a = a.dom(el);
137 switch (typeof a) {
138 case 'string':
139 case 'number':
140 a = document.createTextNode(a);
141 default:
142 if (a) el.appendChild(a);
143 break;
144 }
145 }
146 var id = name.match(/#([^\.#\s]+)/);
147 if (id) el.id = id[1];
148 var classes = name.match(/\.[^.#$\s]+/g);
149 if (classes)
150 el.className = classes.map(function(c) {
151 return c.slice(1); }).join(' ');
152 var style = name.match(/\$(.+)$/);
153 if (style) el.setAttribute('style', style[1]);
154 return el;
155 };
156
157
158
159
160 function Prefs(options) {
161 var self = this;
162 // Table mapping pref names to arrays of listener functions
163 var listeners = {};
164 // Stores the last known value of each pref that is listened
165 // for so that changes can be detected.
166 var cache = {};
167 // An array of the names that are stored in the cache.
168 var cached_prefs = [];
169
170 function check_for_update(name) {
171 // List of names of prefs to check if they have changed.
172 var names_to_check = (name) ? [name] : cached_prefs;
173 for (var i = 0; i < names_to_check.length; i++) {
174 name = names_to_check[i];
175 var old_value = cache[name];
176 var new_value = self[name];
177 if (old_value !== new_value) { // There has been a change
178 log("Change detected for pref " + name);
179 log(" old value: %o", old_value);
180 log(" new value: %o", new_value);
181 var funcs = listeners[name];
182 if (funcs) {
183 for (var j = 0; j < funcs.length; j++) {
184 funcs[j](new_value, old_value, name);
185 }
186 }
187 cache[name] = new_value;
188 }
189 }
190 }
191
192 if (options.live_update) {
193 window.setInterval(function() {
194 check_for_update();
195 }, options.live_update_interval || 200);
196 }
197
198 this.create = function(name, default_) {
199 if ("create|make_checkbox|listen".indexOf(name)>-1)
200 throw "The name " + name + " is reserved.";
201
202 // `is_number` is used to enable a hack that stores
203 // numbers as strings, allowing the storage of any
204 // JavaScript number, not just ints.
205 var is_number = (typeof(default_) == 'number');
206
207 self.__defineGetter__(name, function() {
208 var val = GM_getValue(name, default_);
209 if (is_number) val = parseFloat(val, 10);
210 return val;
211 });
212
213 self.__defineSetter__(name, function(value) {
214 log("Pref " + name + " set");
215 cache[name] = self[name];
216 var set_value = (is_number) ? '' + value : value;
217 GM_setValue(name, set_value);
218 if (name in listeners) {
219 check_for_update(name);
220 }
221 return value;
222 });
223
224 log("Pref created: " + name);
225 };
226
227 // Set a function to be called whenever the pref is
228 // changed, with its value, old value, and name.
229 this.listen = function(name, func) {
230 if (!(name in listeners)) { // Hasn't been listened to before.
231 listeners[name] = [];
232 // Initialize the cache so changes can be detected.
233 cache[name] = self[name];
234 cached_prefs.push(name);
235 }
236 listeners[name].push(func);
237 log("Listener added to " + name);
238 };
239
240 // Make a checkbox that is linked to the pref `name`.
241 // Boolean prefs only, please.
242 this.make_checkbox = function(name) {
243 var cb = document.createElement('input');
244 cb.setAttribute('type', 'checkbox');
245 cb.checked = self[name];
246 cb.addEventListener('DOMActivate', function() {
247 self[name] = this.checked;
248 }, false);
249 // Register
250 self.listen(name, function(value, old_value, name) {
251 cb.checked = value;
252 });
253 return cb;
254 };
255
256 // Link an <input> or <textarea> to a pref.
257 // Textual prefs only.
258 this.link_text_field = function(name, input, overwrite/*=false*/) {
259 function update() { self[name] = input.value; }
260 input.addEventListener("change", update, false);
261 input.addEventListener("keypress", update, false);
262 input.addEventListener("DOMFocusOut", update, false);
263 input.addEventListener("DOMActivate", update, false);
264 document.addEventListener("unload", update, false);
265
266 self.listen(name, function(value, old_value, name) {
267 input.value = value;
268 });
269
270 if (overwrite) { // Overwrite
271 input.value = self[name];
272 } else {
273 self[name] = input.value;
274 }
275 };
276 }
277
278
279 function now() {
280 return (new Date()).getTime();
281 }
282
283
284
285
286
287
288
289
290
291 // Is this page part of the abominable Horizon iPac system?
292 var is_ipac_page = document.location.href.match(/lpl.lacrosse.lib.wi.us\//i);
293
294 function is_login_page() {
295 return is_ipac_page && document.forms[0] && document.forms[0].name == 'security';
296 }
297
298 function is_remote_auth_page() {
299 return document.location.href.match(/http:\/\/lplcat\.lacrosse\.lib\.wi\.us\/rpa\/webauth\.exe/i);
300 }
301
302
303 // Detect a failed login attempt.
304 function login_failed() {
305 return document.body.textContent.indexOf("Login failed. Please check your barcode and pin and try again.") > -1;
306 };
307
308 (function main(){
309
310 var prefs = new Prefs({live_update: true});
311 prefs.create("change_fields", true);
312 prefs.create("remember_info", false);
313 prefs.create("disable_timer", true);
314 prefs.create("auto_login", false);
315 prefs.create("barcode", '');
316 prefs.create("pin", '');
317 prefs.create("hide_null", true);
318 prefs.create("fix_nonlink_styles", true);
319 prefs.create("fix_background_image", true);
320 prefs.create("fix_readers_guide_link", true);
321 prefs.create("fix_readers_guide_link_style", true);
322 prefs.create("fix_community_resources_tab_text", true);
323
324 prefs.create("fix_more_search_options_title", true);
325 prefs.create("fix_more_search_options_link", true);
326 prefs.create("fix_more_search_options_history", true);
327
328 prefs.create("fix_title", true);
329 prefs.create("end_titles_with_lpl", true);
330
331 prefs.create("fix_js_links", true);
332
333 prefs.create("maintain_session", true);
334 prefs.create("maintain_session_interval", 5000); // How often to check whether the server should be pinged.
335 prefs.create("session_timeout_interval", 1000 * 60); // How often to ping the server; default 1 min.
336
337 // We set the defaults of both of these to now() because
338 // only the one (if any) that applies to this page will
339 // be accessed.
340 prefs.create("last_catalog_access", now());
341 prefs.create("last_cr_access", now());
342
343
344
345
346 function get_prefs_interface() {
347 var checkboxes = [
348 // [pref_name, checkbox_label, long_desc],
349 ["change_fields", "Change field types", "Make the login form's password fields normal text fields."],
350 ["remember_info", "Remember my info", "Remember my library card number and PIN."],
351 ["auto_login", "Automatically log in", "Fill and submit any login form encountered."],
352 ["hide_null", "Hide null", 'Hide the "null" on the borrower overview page.'],
353 ["fix_nonlink_styles", "Fix the appearance of nonlink text", "Force only links to look like links"],
354 ["fix_background_image", "Fix background image", "Stop the flowery red background from tiling on very wide monitors"],
355 //["fix_readers_guide_link_style", 'Fix "Reader\'s Guide" link style', "Makes the \"Reader's Guide\" link on the Information Toolbox page look unvisited."],
356 //["fix_community_resources_tab_text", "Fix Community Resources tab text", ""],
357 ["fix_more_search_options_title", "Fix \"More Search Options\" title", "Removes the text \"Click Here For ;\" from the page title."],
358 ["fix_more_search_options_link", "Fix \"More Search Options\" link", "Remove \"Click For\" from the link text."],
359 ["fix_more_search_options_history", "Fix \"More Search Options\" history", "Fix \"More Search Options\" search type in the search history display."],
360 ["fix_title", "Add page titles", "Add appropriate page titles to pages that lack them"],
361 ["end_titles_with_lpl", "End page titles with \u201cLa Crosse Public Library\u201d", "For pages for which titles are added"],
362 ["fix_js_links", "Fix \"javascript:\" links", "Modify JavaScript-protocol links, allowing them to be middle-clicked when possible."],
363 ["disable_timer", "Disable timer", "Disable the client-side code that automatically logs you out after about five minutes by sending you to the logout page. This prevents a new page being suddenly loaded without you clicking on a link or button."],
364 ["maintain_session", "Maintain session", "Prevent the session from expiring on the server by periodically sending it requests. This stops the server from automatically logging you out after about five minutes.\xA0 Note: this feature functions by making periodic requests to the server, which may create congestion on limited bandwith Internet links."]
365 ];
366
367 var homepage_link = El("A", "Home Page");
368 homepage_link.href = "http://freecog.net/2006/lplfixup/";
369 var checkbox_container = El("DIV.checkboxes");
370
371 var barcode_input = El("INPUT");
372 var pin_input = El("INPUT");
373 barcode_input.type = pin_input.type = "text";
374 barcode_input.size = 16; pin_input.size = 4;
375 prefs.link_text_field("barcode", barcode_input, true);
376 prefs.link_text_field("pin", pin_input, true);
377 var clear_button = El("BUTTON", "Clear");
378 clear_button.addEventListener("DOMActivate", function() {
379 prefs.barcode = '';
380 prefs.pin = '';
381 }, false);
382 var info_container = El("DIV.info",
383 El("LABEL", "Card number: ", barcode_input),
384 El("LABEL", "\xA0\xA0\xA0\xA0 PIN: ", pin_input),
385 "\xA0\xA0\xA0\xA0 ", clear_button
386 );
387
388 var close_button = El("BUTTON.close_button", "Close");
389
390 var container = El("DIV#lplfixup_prefs",
391 El("H1", "LPLFixup Options"),
392 El("P", homepage_link),
393 info_container,
394 checkbox_container,
395 El("DIV.bottombar",
396 close_button,
397 El("P", "Your changes take place immediately.")
398 )
399 );
400
401 checkboxes.forEach(function(item) {
402 checkbox_container.appendChild(El("P.checkbox", El("LABEL", prefs.make_checkbox(item[0]), " ", item[1]), item[2]));
403 });
404
405
406 close_button.addEventListener("DOMActivate", function(evt) {
407 var event = document.createEvent("Event");
408 event.initEvent("LPLFixup_Close", false, true);
409 container.dispatchEvent(event);
410 }, false);
411
412 return container;
413 }
414
415
416 // A button to forget saved info
417 var forget_button = El("BUTTON", "Forget");
418 if (!prefs.barcode && !prefs.pin) {
419 forget_button.disabled = true;
420 }
421 forget_button.addEventListener("DOMActivate", function(evt) {
422 prefs.barcode = '';
423 prefs.pin = '';
424 alert("Your barcode and PIN have been forgotten.");
425 forget_button.disabled = true;
426 evt.stopPropagation();
427 evt.preventDefault();
428 }, false);
429
430 var homepage_link = El("A", "LPLFixup Home Page");
431 homepage_link.setAttribute('href', 'http://freecog.net/2006/lplfixup/');
432
433 var prefs_dialog = null;
434 var prefs_button = El("BUTTON.prefs_button", "More Options");
435 prefs_button.addEventListener("DOMActivate", function(evt) {
436 if (!prefs_dialog) {
437 prefs_dialog = get_prefs_interface();
438 }
439 document.body.appendChild(prefs_dialog);
440 evt.stopPropagation();
441 evt.preventDefault();
442 prefs_button.disabled = true;
443 prefs_dialog.addEventListener("LPLFixup_Close", function(evt) {
444 document.body.removeChild(prefs_dialog);
445 prefs_button.disabled = false;
446 }, false);
447 }, false);
448
449
450 /////////////////////////////////
451 ////// LOGIN FORM HANDLING //////
452 /////////////////////////////////
453
454 // The login form.
455 var form = document.forms[0];
456 // The input fields.
457 var barcode_input, pin_input, message_container;
458
459 function restore_info() {
460 if (prefs.remember_info) {
461 log("Restoring info");
462 barcode_input.value = prefs.barcode;
463 pin_input.value = prefs.pin;
464 }
465 }
466
467 // Set the event listener to save the barcode and PIN
468 // when the form is submitted.
469 function set_save_listener() {
470 form.addEventListener('submit', function() {
471 // Save info
472 if (prefs.remember_info) {
473 log("Saving info");
474 prefs.barcode = barcode_input.value;
475 prefs.pin = pin_input.value;
476 }
477 }, false);
478 }
479
480 function get_message(details) {
481 return El("P", El("STRONG", "Unable to automatically log in: " + details + "."));
482 }
483
484 const NEED_MORE_INFO = "please complete your login information";
485 const ATTEMPT_FAILED = "detected failed attempt. Please correct your information";
486
487 if (is_login_page()) { // Login form
488 log("This is an iPac login page.");
489
490 form = document.forms[0];
491 barcode_input = form.elements.namedItem('sec1');
492 pin_input = form.elements.namedItem('sec2');
493
494
495 // I like to be able to see my typos, thank you.
496 if (prefs.change_fields)
497 barcode_input.type = pin_input.type = 'text';
498
499 restore_info();
500
501 // Auto-submit
502 if (prefs.auto_login) {
503 var details;
504 if (!prefs.barcode || !prefs.pin) {
505 details = NEED_MORE_INFO;
506 } else if (login_failed()) {
507 details = ATTEMPT_FAILED;
508 } else {
509 form.submit();
510 return; // No need to continue
511 }
512 var msg = get_message(details);
513 msg.style.color = 'red';
514 var tbody = pin_input.parentNode.parentNode.parentNode;
515 tbody.parentNode.style.width = '50%';
516 var cell = tbody.lastChild.firstChild;
517 cell.replaceChild(msg, cell.firstChild);
518 }
519
520 set_save_listener();
521
522 } else if (is_remote_auth_page()) {
523 log("This is a remote authentication page.");
524
525 form = document.forms[0];
526
527 // Is this the library selection page?
528 if (form.elements.namedItem("lb").type != "hidden") {
529 // Then we'll fill it and submit it.
530 form.elements.namedItem("lb").value = "LACRS";
531 if (prefs.auto_login) {
532 form.submit();
533 }
534 return;
535 } else {
536 barcode_input = form.elements.namedItem('h1');
537 pin_input = form.elements.namedItem('h2');
538
539 if (prefs.change_fields)
540 pin_input.type = 'text';
541
542 restore_info();
543
544 if (prefs.auto_login) {
545 var details;
546 if (!prefs.barcode || !prefs.pin) {
547 details = NEED_MORE_INFO;
548 } else if (document.body.innerHTML.match(/Sorry, Your identification is not recognized/)) {
549 details = ATTEMPT_FAILED;
550 } else {
551 // We can login!
552 form.submit();
553 return; // No need to continue processing the page.
554 }
555 var msg = get_message(details);
556 msg.style.color = 'red';
557 input.parentNode.appendChild(msg);
558 }
559
560 set_save_listener();
561 }
562 }
563
564
565 // Set up the inline prefs.
566 var inline_prefs = El("DIV#lplfixup_inline_prefs",
567 El("STRONG", "LPLFixup"),
568 El("LABEL", prefs.make_checkbox("remember_info"), " Remember my info ", forget_button),
569 El("LABEL", prefs.make_checkbox("auto_login"), " Automatically login"),
570 El("LABEL", prefs.make_checkbox("disable_timer"), " Disable automatic logout"),
571 " ", prefs_button, homepage_link
572 );
573 document.body.appendChild(inline_prefs);
574
575
576 // Insert the style sheet
577 GM_addStyle([
578 '#lplfixup_prefs {',
579 'font-size: .8em;',
580 'text-align: left;',
581 'border: 2px solid black;',
582 'padding: 10px;',
583 'position: absolute;',
584 'left: 100px;',
585 'right: 100px;',
586 'top: 100px;',
587 'margin-bottom: 100px;',
588 'background: white;',
589 '}',
590 '#lplfixup_prefs h1 {',
591 'margin-top: 0;',
592 'line-height: 1;',
593 'text-align: center;',
594 '}',
595 '#lplfixup_prefs div.checkboxes {',
596 '-moz-column-count: 2;',
597 '}',
598 '#lplfixup_prefs p {',
599 'text-indent: -25px;',
600 'margin-left: 25px;',
601 '}',
602 '#lplfixup_prefs p label {',
603 'display: block;',
604 'font-size: 1.2em;',
605 '}',
606 '#lplfixup_prefs .info {',
607 'font-size: 1.2em;',
608 '}',
609 '#lplfixup_prefs .close_button {',
610 'float: right;',
611 '}',
612 '#lplfixup_prefs .bottombar {',
613 'margin-top: 20px;',
614 '}',
615 '#lplfixup_prefs button,',
616 '#lplfixup_inline_prefs button {',
617 'font-size: inherit;',
618 'font-family: inherit;',
619 '}',
620 '#lplfixup_inline_prefs label {',
621 'white-space: nowrap;',
622 '}',
623 '#lplfixup_prefs a,',
624 '#lplfixup_inline_prefs a {',
625 'white-space: nowrap;',
626 'margin-left: 2em;',
627 '}',
628 '#lplfixup_inline_prefs label {',
629 'margin-left: 1em;',
630 '}',
631 '#lplfixup_inline_prefs button.prefs_button {',
632 'margin-left: 2em;',
633 '}',
634 '#lplfixup_inline_prefs {',
635 'font-size: .7em;',
636 'text-align: center;',
637 'margin-bottom: 10px;',
638 'width: 80%;', // It overflows the <body> without this width declaration in narrow windows.
639 'margin-left: auto; margin-right: auto;', // Center
640 '}',
641 ].join('\n'));
642
643
644 // Set up the timer killer
645 var orig_timer = unsafeWindow.Timer;
646
647 function set_timer(value) {
648 if (value) {
649 log("NOOP Timer set.");
650 unsafeWindow.location.href = "javascript:" +
651 encodeURIComponent("Timer = function(){}; void(0);");
652 } else {
653 unsafeWindow.location.href = "javascript:" +
654 encodeURIComponent("Timer = " + orig_timer.toSource().
655 replace(/closeTime/g, 'window.closeTime') + "; Timer(); void(0);"
656 );
657 log("Original Timer set.");
658 }
659 }
660
661 prefs.listen("disable_timer", set_timer);
662 set_timer(prefs.disable_timer);
663
664
665
666 //////////////////////////////
667 // SET THE DOCUMENT TITLE //
668 //////////////////////////////
669
670 // Titles in the two major sections of the interface are displayed
671 // differently in each area, with added versions for the serch results
672 // pages and the individual item pages.
673 //
674 // "Search the Catalog" -- search forms
675 // An <a class="boldBlackFont2">
676 // ``get_serach_form_title()``
677 // "My Library Record" -- borrower information
678 // An <a class="big">
679 // ``get_myrecord_title()``
680 // Search Results
681 // An <a class="boldBlackFont3">
682 // ``get_search_results_title()``
683 // Item Record Page
684 // An <a class="largeAnchor">
685 // ``get_record_title()``
686 //
687 // All these functions return null if they don't find an appropriate
688 // title.
689 //
690 // I now suspect that a better architecture might have been to figure
691 // out which link in the navigation is highlighted--but oh well.
692
693
694 // A helper function for finding <a>s with a particular class.
695 // <a>s with presentational class names are used on iPac pages much
696 // like <span>s might be on a site designed by a rational mind.
697 // Fortunately, Horizon doesn't seem to have any concept of
698 // multiple classes, and doesn't surround them with whitespace, so
699 // a simple equality comparison works.
700 function get_as_by_class_name(class_name) {
701 if (typeof(class_name) == 'string')
702 class_name = new RegExp('(^|\s)' + class_name + '(\s|$)');
703 return Array.slice(document.getElementsByTagName('a') || []).filter(function(a) {
704 return a.className.match(class_name);
705 });
706 }
707
708 // Maps the title pulled from the page content to the title
709 // used when the title is inferred from the URL. This is used
710 // instead of the title in the content where possible for consistency
711 // with the page titles generated elsewhere.
712 var myrecord_title_remap = {
713 'Items Out': 'Checked Out',
714 'Hold Requests': 'Holds',
715 'Blocks': 'Fines/Blocks',
716 'Profile': 'Borrower Information',
717 };
718
719 // Gets the text of the first <a> with a class of "big" -- this
720 // is appropriate as a document title for "My Library Record" pages.
721 function get_myrecord_title() {
722 var big_as = get_as_by_class_name('big');
723 if (big_as[0]) {
724 var text = big_as[0].textContent;
725 var title = text.replace(/\s-.*/, '');
726 // For consistency's sake, map the title to whatever we use
727 // when it's inferred from the URL.
728 return myrecord_title_remap[title] || title;
729 }
730 return null;
731 }
732
733 // Get a document title on a "My List" listing
734 // These listings look much like search result pages from
735 // a HTML perspective.
736 function get_mylist_title() {
737 var containers = get_as_by_class_name('boldBlackFont2');
738 if (containers[0] && containers[0].textContent.match(/\d+\s+titles/)) {
739 var titles = containers[0].textContent;
740 if (titles.match(/^1\s+titles$/)) {
741 titles = '1 title';
742 // Might as well fix this, while we're at it.
743 containers[0].firstChild.nodeValue = titles;
744 }
745 var list_name = containers[0].parentNode.parentNode.previousSibling.firstChild.textContent;
746 return list_name + " (" + titles + ")";
747 } else {
748 log("No containers.");
749 }
750
751 // New list name prompt
752 if (document.forms && document.forms.namedItem("ipac") &&
753 document.forms.namedItem("ipac").elements.namedItem("moveto_listkey") &&
754 document.forms.namedItem("ipac").elements.namedItem("moveto_listkey").value == "ipac_new_saved_list") {
755 return "Enter new list name";
756 }
757
758 log("Not a \"My List\" listing.");
759 return null;
760 }
761
762
763 function get_mylist_management_title() {
764 // We find this page by the names of the table columns.
765 if (document.body.textContent.match(/ListsListTitlesCreatedExpires/)) {
766 return 'Manage Lists';
767 }
768 return null;
769 }
770
771 // Like `myrecorrd_title_remap`, but for search form pages.
772 var search_form_title_remap = {
773 'BASIC SEARCHES': 'Basic Search',
774 'Click Here For ;More Search Options': 'More Search Options',
775 };
776
777 // Get a document title on a search form page.
778 function get_search_form_title() {
779 var containers = get_as_by_class_name('boldBlackFont2');
780 if (containers[0]) {
781 var a = containers[0];
782 var text = a.textContent.replace(/^[\xA0\s]+|\s+$/g, "");
783 return search_form_title_remap[text] || text;
784 }
785 return null;
786 }
787
788 // Get a document title on a search results page.
789 function get_search_result_title() {
790 var bbf3_as = get_as_by_class_name('boldBlackFont3');
791 if (bbf3_as[0]) {
792 var a1 = bbf3_as[0];
793 var bolded = a1.parentNode.parentNode.nextSibling.getElementsByTagName('b');
794 if (bolded.length == 1) {
795 // Just the query
796 var info = bolded[0].textContent;
797 } else if (bolded.length == 2) {
798 // Include the number of results
799 var info = bolded[1].textContent + " (" + bolded[0].textContent + ")";
800 }
801 return a1.textContent.replace(/^\s+/, '') + ": " + info
802 }
803 return null;
804 }
805
806 // Get a document title for an item record page.
807 function get_record_title() {
808 var anchors = get_as_by_class_name('largeAnchor');
809 if (anchors[0]) {
810 return anchors[0].textContent.replace(/^\s+|\s+$/, '');
811 }
812 }
813
814 // Get the document title for record in the Community Resources
815 // database.
816 function get_community_resources_record_title() {
817 // These records lack any prominent header--they're just flat DB fields.
818 // All record pages seem to contain the text "Question/Headline:", so
819 // that'll be used to detect them.
820 if (document.body.textContent.indexOf("Question/Headline:") > -1) {
821 // The text relevant to the title is contained in the <a class="normalBlackFont1">
822 // after the one containing "Question/Headline: ". This seems to be the third one,
823 // but I'm not going to count on it.
824 var containers = get_as_by_class_name("normalBlackFont1");
825 var extract_next = false; // set to true when "Question/Headline" is encountered.
826 for (var i = 0; i < containers.length; i++) {
827 if (extract_next) {
828 return containers[i].textContent.replace(/^\s+|\s+$/g, ""); // Should ellipsize this?
829 } else if (containers[i].textContent.match(/Question\/Headline:/i)) {
830 extract_next = true;
831 }
832 }
833
834 // Nothing was extracted. Fallback:
835 log("Unable to extract a title for this Community Resources page.");
836 return "Item Record";
837
838 // The fallback will also be displayed for MARC records. I'm not going to bother
839 // changing that unless someone requests it.
840 }
841 return null;
842 }
843
844
845 // Get a document title for the Information Toolbox page.
846 function get_information_toolbox_title() {
847 // Is this the information toolbox page? Since it has
848 // no content, we find out by examining the navigation.
849 var pseudo_spans = get_as_by_class_name("TabActive");
850 if (pseudo_spans[0] && pseudo_spans[0].title == "Great databases") {
851 return "Information Toolbox";
852 } else {
853 return null;
854 }
855 }
856
857
858 // Dict mapping the URL components 'menu' and 'submenu' to page titles.
859 var url_to_title = {
860 'search': {
861 DEFAULT: 'Basic Search',
862 'basic_search': 'Basic Search',
863 'advanced': 'More Search Options',
864 'power': 'Power Search',
865 'history': 'Search History'
866 },
867 'account': {
868 DEFAULT: 'Account Overview',
869 'overview': 'Account Overview',
870 'itemsout': 'Checked Out',
871 'holds': 'Holds',
872 'blocks': 'Fines/Blocks',
873 'info': 'Borrower Information'
874 },
875 'Information%20Resources': {
876 DEFAULT: 'Information Toolbox'
877 },
878 'mylist': {
879 DEFAULT: 'My List'
880 }
881 };
882
883
884 // Infer from the URL -- this would be used in the
885 // general case, except it doesn't work on POST or login pages.
886 function get_title_from_url() {
887 var url = document.location.href;
888 // There must be a querystring, and it can't be one for a logout page
889 if (!url.match(/\/ipac.jsp$|[\?&]logout=true(&|$)/)) {
890 try {
891 var menu = url.match(/[?&]menu=([\w\d%]+)(&|$)/)[1];
892 var submenu = 'DEFAULT';
893 try {
894 var submenu = url.match(/[?&]submenu=(\w+)(&|$)/)[1];
895 } catch(e) { /* go with DEFAULT */ }
896 return url_to_title[menu][submenu] || url_to_title[menu].DEFAULT;
897 } catch(e) {
898 log("Error parsing the querystring to get a title: " + e);
899 log(" menu = " + menu);
900 log(" submenu = " + submenu);
901 }
902 }
903 log("Not able to parse the querystring for a title.");
904 return null;
905 };
906
907 // Get the title if this is a login page.
908 function get_login_title() {
909 return is_login_page() ? 'Login' : null;
910 };
911
912 var title_getters = [
913 get_login_title,
914 get_mylist_title,
915 get_mylist_management_title,
916 get_search_result_title,
917 get_record_title,
918 get_community_resources_record_title,
919 get_title_from_url,
920 // Fallbacks
921 get_information_toolbox_title,
922 get_search_form_title,
923 get_myrecord_title,
924 ];
925
926 // Code poetry:
927 function fix_title() {
928 // Does the document need a title?
929 var need = (!document.title || document.title === "iPac2.0")
930 // Can it be fixed?
931 var ability = document.location.href.match(/\/ipac.jsp?/);
932 return need && ability;
933 }
934
935 if (prefs.fix_title && fix_title()) {
936 // Kill me now ;-)
937 var title, i = 0;
938 while (title_getters[i] && !(title = title_getters[i]())) { i++; }
939
940 if (!title) {
941 // Fallback, if nothing else is found
942 title = 'Online Catalog';
943 log("No title found \u2014 a fallback will be used.");
944 }
945
946 if (prefs.end_titles_with_lpl) {
947 title += ' \u2014 La Crosse Public Library';
948 }
949
950 document.title = title;
951 log("document.title set to \u201c" + title + "\u201d")
952 }
953
954
955
956 ////////////////////////////////////////////
957 // HIDE THAT 'null' ON THE ACCOUNT PAGE //
958 ////////////////////////////////////////////
959
960 if (prefs.hide_null) {
961 var ps = document.getElementsByTagName('p');
962 if (ps && ps[1] && ps[1].textContent == 'null') {
963 ps[1].parentNode.removeChild(ps[1]);
964 }
965 }
966
967 //////////////////////////////////////////
968 // MAKE NON-LINKS NOT LOOK LIKE LINKS //
969 //////////////////////////////////////////
970
971 if (prefs.fix_nonlink_styles && is_ipac_page) {
972 GM_addStyle([
973 'a[class$=Anchor]:not([href]) {',
974 'font-weight: normal;',
975 'color: black;',
976 '}',
977 'a[class$=Anchor]:not([href]):hover {',
978 'text-decoration: none;',
979 '}',
980 'a[class$=Anchor]:not([href]):active {',
981 'font-style: normal;',
982 '}'
983 ].join('\n'));
984 }
985
986 /////////////////////////////////////////////////////////////////////
987 // REMOVE "Click Here For ;" FROM THE "More Search Options" PAGE //
988 /////////////////////////////////////////////////////////////////////
989
990 if (prefs.fix_more_search_options_title && is_ipac_page) {
991 var a_tags = get_as_by_class_name("boldBlackFont2");
992 if (a_tags.length) {
993 var a = a_tags[0];
994 if (a.textContent.match(/Click Here For ;More Search Options/)) {
995 a.firstChild.textContent = "\xA0More Search Options";
996 log('"More Search Options" page title corrected');
997 }
998 }
999 }
1000
1001 ////////////////////////////////
1002 // FIX THE BACKGROUND IMAGE //
1003 ////////////////////////////////
1004
1005 if (prefs.fix_background_image && !is_ipac_page) {
1006 GM_addStyle([
1007 'body[background$="redbackground.gif"] {',
1008 'background: white url(/images/redbackground.gif) repeat-y top left;',
1009 '}'
1010 ].join('\n'));
1011 log("Any red-background issue has been corrected.");
1012 }
1013
1014
1015 //////////////////////////////
1016 // FIX READER'S GUIDE LINK //
1017 //////////////////////////////
1018
1019 if (prefs.fix_readers_guide_link || prefs.fix_readers_guide_link_style) {
1020 get_as_by_class_name('navBarCurrent').forEach(function(a) {
1021 if (a.textContent == "Readers' Guide") {
1022 if (prefs.fix_readers_guide_link && a.href == "http://lplcat.lacrosse.lib.wi.us/rpa/webauth.exe?rs=RG") {
1023 a.href += '&lb=LACRS';
1024 }
1025 if (prefs.fix_readers_guide_link_style) {
1026 a.className = 'navBarAnchor';
1027 }
1028 }
1029 });
1030 }
1031
1032 ////////////////////////////////////////////
1033 // FIX THE COMMUNITY RESOURCES TAB TEXT //
1034 ////////////////////////////////////////////
1035
1036 if (prefs.fix_community_resources_tab_text) {
1037 get_as_by_class_name('TabActive').forEach(function(a) {
1038 if (a.textContent == "Local news index&Fast Facts")
1039 a.firstChild.nodeValue = 'Local news index & Fast Facts';
1040 });
1041 }
1042
1043 /////////////////////////////////////////
1044 // FIX MORE SEARCH OPTIONS LINK TEXT //
1045 /////////////////////////////////////////
1046
1047 // Remove "Click For" from the "Click For More Search Options" link.
1048 if (prefs.fix_more_search_options_link) {
1049 get_as_by_class_name('navBarAnchor|navBarCurrent').forEach(function(a) {
1050 if (a.textContent == "Click ForMore Search Options") {
1051 a.innerHTML = "More Search Options";
1052 log('Fixed "More Search Options" link text.');
1053 }
1054 });
1055 }
1056
1057 /////////////////////////////////////////////////////////////
1058 // FIX "Click For;More Search Options" IN SEARCH HISTORY //
1059 /////////////////////////////////////////////////////////////
1060
1061 if (prefs.fix_more_search_options_history) {
1062 // Is this a search history page? -- &menu=search&submenu=history
1063 var url = document.location.href;
1064 if (url.match(/[\?&]menu=search(&|$)/) && url.match(/[\?&]submenu=history(&|$)/)) {
1065 get_as_by_class_name('normalBlackFont1').forEach(function(a) {
1066 if (a.textContent == "Click For;More Search Options") {
1067 a.firstChild.nodeValue = "More Search Options";
1068 }
1069 });
1070 log('Done fixing the search history page\'s "More Search Options" entries.');
1071 }
1072 }
1073
1074
1075 ////////////////////////////
1076 // FIX JAVASCRIPT LINKS //
1077 ////////////////////////////
1078
1079 // Function reference:
1080 /*
1081
1082 buildReturnPageNewList(url, return_url)
1083 Submits the form "buildLink", which has just one field, named
1084 "returnURL," which is filled with the `return_url` parameter.
1085 The action of the form is set to `url`, and the form is POST'ed.
1086 (Note that the url is unescape()'d when navigator.appName ==
1087 'Netscape', too.)
1088 Replacement action: simply use `url` as the href of the link.
1089 This may break the "Return to results" link, but who cares?
1090 That's what the back button's for.
1091 ReturnSearchPage(url)
1092 Submits either the form "SearchResult" or the form "buildLink"
1093 with `url` as the action, or falls back to setting document.location
1094 to `url`. `url` is unescape()'d.
1095 Replacement action: href_from_string_arg()
1096 buildNewList(url, return_url, summary)
1097 I'm not exactly sure what this is trying to to. I guess that
1098 it is accumulating a breadcrum trail of history. That's pointless,
1099 since it's already implemeneted in the form of the back button, and
1100 will likely break when multiple tabs are used anyway. Axed.
1101 Replacement action: href_from_string_arg()
1102 loginIntoOrOutOfAccount(url, return_url)
1103 This seems to combine buildReturnPageNewList and buildNewList.
1104 Replacement action: href_from_string_arg()
1105 AddCopy(bkey, ikey, pos)
1106 This is part of the booklist system. It's pure JavaScript, so it
1107 shouldn't be replaced.
1108 buildMyList(url, return_url)
1109 Yet another thing that seems to do whatever buildReturnPageNewList
1110 and buildNewList do.
1111 Replacement action: href_from_string_arg()
1112
1113 */
1114
1115 // Extracts the body of a string literal as if it had been eval()'d.
1116 // This is probably painfully slow.
1117 function eval_string(string) {
1118 var quote = string[0];
1119 if (!(quote == "'" || quote == '"') || string[string.length-1] != quote || string.length < 2)
1120 throw new Error("This doesn't look like a string literal: " + string);
1121 string = string.slice(1, -1);
1122 var chunks = string.split(/(\\(?:['"\/bfnrt]|u[a-zA-Z0-9]{4}|U[a-zA-Z0-9]{8}))/);
1123 for (var i = 0; i < chunks.length; i++) {
1124 if (chunks[i][0] == '\\') {
1125 chunks[i] = eval(quote + chunks[i] + quote);
1126 }
1127 }
1128 return chunks.join('');
1129 }
1130
1131 // Extracts and sets `a`'s href propery to the the text of the first
1132 // single-quoted string in the href.
1133 function href_from_string_arg(a) {
1134 // The string extraction RE isn't right, but the URLs never seem
1135 // to contain escaped apostrophes anyway.
1136 a.href = unescape(eval_string(a.href.match(/'(\\'|[^'])+'/)[0]));
1137 }
1138
1139 function fix_popUpHelp(a) {
1140 a.setAttribute('onclick', a.href.replace(/javascript:/, "") + 'return false;');
1141 href_from_string_arg(a);
1142 }
1143
1144 // Maps link function names to functions that set a valid HREF.
1145 // NOOP functions are provided where it is not possible to provide
1146 // a non-JavaScript equivalent.
1147 var link_functions_to_fixes = {
1148 buildReturnPageNewList: href_from_string_arg,
1149 ReturnSearchPage: href_from_string_arg,
1150 viewlargeimage: function(){}, // NOOP
1151 AddCopy: function(){}, // NOOP
1152 buildNewList: href_from_string_arg,
1153 buildMyList: href_from_string_arg,
1154 popUpHelp: fix_popUpHelp,
1155 loginIntoOrOutOfAccount: href_from_string_arg,
1156 };
1157
1158 if (prefs.fix_js_links) {
1159 // Dispatch to the functions for link types.
1160 Array.slice(document.getElementsByTagName("a")).filter(function(a) {
1161 return ( a.href && a.href.match(/^\s*javascript:/i) );
1162 }).forEach(function(a) {
1163 var func_name = a.href.match(/^\s*javascript:\s*([a-z0-9_$]+)/i)[1];
1164 var func = link_functions_to_fixes[func_name];
1165 if (func) {
1166 func(a, func_name);
1167 if (DEBUG) {
1168 if (a.href.match(/^http:/i)) {
1169 a.href += "#fixed_from_" + func_name;
1170 } else {
1171 a.href += "//fixed from " + func_name;
1172 }
1173 }
1174 } else {
1175 log("Unable to fix a link calling " + func_name);
1176 log(" link was " + a.href);
1177 }
1178 });
1179 log("Link fixing completed");
1180 }
1181
1182
1183 ///////////////////////////
1184 // MAINTAIN THE SESSION //
1185 ///////////////////////////
1186
1187
1188 function is_community_resources_page() {
1189 return document.location.href.match(/^https?:\/\/lpl.lacrosse.lib.wi.us:81\//i);
1190 }
1191
1192 var session_interval_ident = null;
1193 var access_pref_name = is_community_resources_page() ? "last_cr_access" : "last_catalog_access";
1194
1195 function maintain_session() {
1196 if (!prefs.maintain_session) {
1197 session_interval_ident = null;
1198 return;
1199 }
1200
1201 var expires_at = prefs[access_pref_name] + prefs.session_timeout_interval;
1202 if (expires_at <= now()) {
1203 // Pull the url to load out of the navigation (we need one that
1204 // contains the session token).
1205 var link_found = false;
1206 var links = get_as_by_class_name("TabInactive|navBarAnchor");
1207 for (var i = 0; i < links.length; i++) {
1208 if (links[i].href.match(/^https?:\/\/lpl.lacrosse.lib.wi.us/)) {
1209 GM_xmlhttpRequest({
1210 method: 'GET',
1211 url: links[i].href,
1212 onload: function() {
1213 log("Session extension request completed.");
1214 prefs[access_pref_name] = now();
1215 },
1216 onerror: function() {
1217 log("Error encountered when making the session extension request.");
1218 }
1219 });
1220 link_found = true;
1221 break;
1222 }
1223 }
1224
1225 if (link_found) {
1226 log("Request sent to extend the session.");
1227 } else {
1228 log("Couldn't find a link to extend the session with");
1229 }
1230 }
1231
1232 session_interval_ident = window.setTimeout(maintain_session, prefs.maintain_session_interval);
1233 }
1234
1235 if (is_ipac_page) {
1236 log("Starting session maintenance loop.");
1237 prefs[access_pref_name] = now();
1238 maintain_session();
1239 prefs.listen("maintain_session", function(value, old_value, name) {
1240 if (value && !old_value) maintain_session();
1241 if (!value && session_value_ident) window.clearTimeout(session_value_ident);
1242 });
1243 }
1244
1245 })();
© 2004–2010 Tom W. Most