//= require if (!Prototype || Prototype.Version.indexOf('1.6') !== 0) { throw "This script requires Prototype >= 1.6."; } Object.isDate = function(object) { return object instanceof Date; }; /** * class Cookie * Creates a cookie. **/ var Cookie = Class.create({ /** * new Cookie(name, value[, expires]) * * - name (String): The name of the cookie. * - value (String): The value of the cookie. * - expires (Number | Date): Exact date (or number of days from now) that * the cookie will expire. **/ initialize: function(name, value, expires) { expires = expires || ""; if (Object.isNumber(expires)) { var days = expires; expires = new Date(); expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); } if (Object.isDate(expires)) expires = expires.toGMTString(); if (!Object.isUndefined(expires) && expires !== "") expires = "; expires=" + expires; this.name = name; this.value = value; this.expires = expires; document.cookie = name + "=" + value + expires + "; path=/"; }, toString: function() { return this.value; }, inspect: function() { return "#".interpolate(this); } }); /** * Cookie **/ Object.extend(Cookie, { /** * Cookie.set(name, value, expires) * * Alias of [[Cookie#initialize]]. **/ set: function(name, value, expires) { return new Cookie(name, value, expires); }, /** * Cookie.get(name) * * Returns the value of the cookie with the given name. * - name (String): The name of the cookie to retrieve. **/ get: function(name) { var c = document.cookie.split(';'); for (var i = 0, cookie; i < c.length; i++) { cookie = c[i].split('='); if (cookie[0].strip() === name) return cookie[1].strip(); } return null; }, /** * Cookie.unset(name) * * Deletes a cookie. * - name (String): The name of the cookie to delete. * **/ unset: function(name) { return Cookie.set(name, "", -1); } }); Cookie.erase = Cookie.unset; if (typeof PDoc === 'undefined') { window.PDoc = { Sidebar: {} }; } // HISTORY MANAGER (sort of) // Polls for changes to the hash. (function() { var PREVIOUS_HASH = null; function poll() { var hash = window.location.hash; if (hash && hash !== PREVIOUS_HASH) { document.fire('hash:changed', { previous: PREVIOUS_HASH, current: hash }); } PREVIOUS_HASH = hash; window.setTimeout(arguments.callee, 100); } Event.observe(window, 'load', poll); })(); Object.extend(PDoc.Sidebar, { getActiveTab: function() { var activeTab = $('sidebar_tabs').down('.active'); if (!activeTab) return null; var href = activeTab.readAttribute('href'); return href.endsWith('menu_pane') ? 'menu_pane' : 'search_pane'; }, // Remember the state of the sidebar so it can be restored on the next page. serialize: function() { var state = $H({ activeTab: PDoc.Sidebar.getActiveTab(), menuScrollOffset: $('menu_pane').scrollTop, searchScrollOffset: $('search_results').scrollTop, searchValue: $('search').getValue() }); return escape(state.toJSON()); }, // Restore the tree to a certain point based on a cookie. restore: function(state) { try { state = unescape(state).evalJSON(); var filterer = $('search').retrieve('filterer'); filterer.setSearchValue(state.searchValue); (function() { $('menu_pane').scrollTop = state.menuScrollOffset; $('search_results').scrollTop = state.searchScrollOffset; }).defer(); } catch(error) { console.log(error); if (!(error instanceof SyntaxError)) throw error; } } }); // Live API search. PDoc.Sidebar.Filterer = Class.create({ initialize: function(element, options) { this.element = $(element); this.options = Object.extend( Object.clone(PDoc.Sidebar.Filterer.DEFAULT_OPTIONS), options || {} ); // The browser's "helpful" auto-complete gets in the way. this.element.writeAttribute("autocomplete", "off"); this.element.setValue(''); // Hitting "enter" should do nothing. this.element.up('form').observe("submit", Event.stop); this.menu = this.options.menu; this.links = this.menu.select('a'); this.resultsElement = this.options.resultsElement; this.observers = { filter: this.filter.bind(this), keydown: this.keydown.bind(this), keyup: this.keyup.bind(this) }; this.menu.setStyle({ opacity: 0.9 }); this.addObservers(); }, addObservers: function() { this.element.observe('keyup', this.observers.filter); }, // Called whenever the list of results needs to update as a result of a // changed search key. filter: function(event) { // Clear the text box on ESC. if (event.keyCode && event.keyCode === Event.KEY_ESC) { this.element.setValue(''); } if (PDoc.Sidebar.Filterer.INTERCEPT_KEYS.include(event.keyCode)) return; // If there's nothing in the text box, clear the results list. var value = $F(this.element).strip().toLowerCase(); if (value === '') { this.emptyResults(); this.hideResults(); return; } var urls = this.findURLs(value); this.buildResults(urls); }, setSearchValue: function(value) { this.element.setValue(value); if (value.strip() === "") { PDoc.Sidebar.Tabs.setActiveTab(0); return; } this.buildResults(this.findURLs(value)); }, // Given a key, finds all the PDoc objects that match. findURLs: function(str) { var results = []; for (var name in PDoc.elements) { if (name.toLowerCase().include(str.toLowerCase())) results.push(PDoc.elements[name]); } return results; }, buildResults: function(results) { this.emptyResults(); results.each( function(result) { var li = this._buildResult(result); this.resultsElement.appendChild(li); }, this); this.showResults(); }, _buildResult: function(obj) { var li = new Element('li', { 'class': 'menu-item' }); var a = new Element('a', { 'class': obj.type.gsub(/\s/, '-'), 'href': PDoc.pathPrefix + obj.path }).update(obj.name); li.appendChild(a); return li; }, emptyResults: function() { this.resultsElement.update(); }, hideResults: function() { PDoc.Sidebar.Tabs.setActiveTab(0); //this.resultsElement.hide(); document.stopObserving('keydown', this.observers.keydown); document.stopObserving('keyup', this.observers.keyup); }, showResults: function() { PDoc.Sidebar.Tabs.setActiveTab(1); //this.resultsElement.show(); document.stopObserving('keydown', this.observers.keydown); this.element.stopObserving('keyup', this.observers.keyup); this.element.observe('keydown', this.observers.keydown); document.observe('keyup', this.observers.keyup); }, keydown: function(event) { if (!PDoc.Sidebar.Filterer.INTERCEPT_KEYS.include(event.keyCode)) return; // Also ignore if any modifier keys are present. if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return; event.stop(); if (event.keyCode === Event.KEY_RETURN) { // Follow the highlighted item, unless there is none. if (!this.highlighted) return; var a = this.highlighted.down('a'); if (a) { window.location.href = a.href; } } else if ([Event.KEY_UP, Event.KEY_DOWN].include(event.keyCode)) { // Is an arrow key. var direction = (Event.KEY_DOWN === event.keyCode) ? 1 : -1; this.highlighted = this.moveHighlight(direction); if (!Prototype.Browser.WebKit) { // If up/down key is held down, list should keep scrolling. // WebKit does this automatically because it fires the keydown // event over and over. this._scrollTimer = window.setTimeout( this.scrollList.bind(this, direction), 1000); } } }, keyup: function(event) { if (this._scrollTimer) { window.clearTimeout(this._scrollTimer); } }, moveHighlight: function(direction) { if (!this.highlighted) { // If there is none, highlight the first result. this.highlighted = this.resultsElement.down('li').addClassName('highlighted'); } else { var method = (direction === 1) ? 'next' : 'previous'; this.highlighted.removeClassName('highlighted'); var adjacent = this.highlighted[method]('li'); // If there isn't an adjacent one, we're at the top or bottom // of the list. Flip it. if (!adjacent) { adjacent = method == 'next' ? this.resultsElement.down('li') : this.resultsElement.down('li:last-of-type'); } adjacent.addClassName('highlighted'); this.highlighted = adjacent; } var h = this.highlighted, r = this.resultsElement; var distanceToBottom = h.offsetTop + h.offsetHeight; if (distanceToBottom > (r.offsetHeight + r.scrollTop)) { // Item is below the visible frame. r.scrollTop = distanceToBottom - r.offsetHeight; } else if (h.offsetTop < r.scrollTop) { // Item is above the visible frame. r.scrollTop = h.offsetTop; } return this.highlighted; }, scrollList: function(direction) { this.moveHighlight(direction); this._scrollTimer = window.setTimeout( this.scrollList.bind(this, direction), 100); } }); Object.extend(PDoc.Sidebar.Filterer, { INTERCEPT_KEYS: [Event.KEY_UP, Event.KEY_DOWN, Event.KEY_RETURN], DEFAULT_OPTIONS: { interval: 0.1, resultsElement: '.search-results' } }); Form.GhostedField = Class.create({ initialize: function(element, title, options) { options = options || {}; this.element = $(element); this.title = title; this.isGhosted = true; if (options.cloak) { // Wrap the native getValue function so that it never returns the // ghosted value. This is optional because it presumes the ghosted // value isn't valid input for the field. this.element.getValue = this.element.getValue.wrap(this.wrappedGetValue.bind(this)); } this.addObservers(); this.onBlur(); }, wrappedGetValue: function($proceed) { var value = $proceed(); return value === this.title ? "" : value; }, addObservers: function() { this.element.observe('focus', this.onFocus.bind(this)); this.element.observe('blur', this.onBlur.bind(this)); var form = this.element.up('form'); if (form) { form.observe('submit', this.onSubmit.bind(this)); } // Firefox's bfcache means that form fields need to be re-initialized // when you hit the "back" button to return to the page. if (Prototype.Browser.Gecko) { window.addEventListener('pageshow', this.onBlur.bind(this), false); } }, onFocus: function() { if (this.isGhosted) { this.element.setValue(''); this.setGhosted(false); } }, onBlur: function() { var value = this.element.getValue(); if (value.blank() || value == this.title) { this.setGhosted(true); } else { this.setGhosted(false); } }, setGhosted: function(isGhosted) { this.isGhosted = isGhosted; this.element[isGhosted ? 'addClassName' : 'removeClassName']('ghosted'); if (isGhosted) { this.element.setValue(this.title); } }, // Hook into the enclosing form's `onsubmit` event so that we clear any // ghosted text before the form is sent. onSubmit: function() { if (this.isGhosted) { this.element.setValue(''); } } }); document.observe('dom:loaded', function() { PDoc.Sidebar.Tabs = new Control.Tabs($('sidebar_tabs')); var searchField = $('search'); if (searchField) { var filterer = new PDoc.Sidebar.Filterer(searchField, { menu: $('api_menu'), resultsElement: $('search_results') }); searchField.store('filterer', filterer); } // Prevent horizontal scrolling in scrollable sidebar areas. $$('.scrollable').invoke('observe', 'scroll', function() { this.scrollLeft = 0; }); var sidebarState = Cookie.get('sidebar_state'); if (sidebarState) { PDoc.Sidebar.restore(sidebarState); } new Form.GhostedField(searchField, searchField.getAttribute('title'), { cloak: true }); }); Event.observe(window, 'unload', function() { Cookie.set('sidebar_state', PDoc.Sidebar.serialize()); });