spahql
Version:
A query language and data model for deep Javascript object structures.
478 lines (392 loc) • 12.8 kB
JavaScript
//= require <prototype>
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 "#<Cookie #{name}:#{value}>".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());
});