// Install some useful jQuery extensions that we use a lot
$.support.touch = 'ontouchstart' in window;
$.extend($.fn, {
orNull: function() {
return this.length > 0 ? this : null;
},
findAndSelf: function(selector) {
return this.find(selector).add(this.filter(selector));
}
});
// Little Helpers
function hyphenate(str) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
function isVisible(el) {
if (el.is(':hidden'))
return false;
var viewTop = $(window).scrollTop();
var viewBottom = viewTop + $(window).height();
var top = el.offset().top;
var bottom = top + el.height();
return top >= viewTop && bottom <= viewBottom
|| top <= viewTop && bottom >= viewTop
|| top <= viewBottom && bottom >= viewBottom;
}
function smoothScrollTo(el, callback) {
$('html, body').animate({
scrollTop: el.offset().top
}, 250, callback);
}
var behaviors = {};
behaviors.hiDPI = function() {
// Turn off hiDPI for all touch devices for now, until the site is built
// true to scale.
if ($.support.touch)
$('canvas').attr('hidpi', 'off');
};
behaviors.sections = function() {
var toc = $('.toc');
var checks = [];
var active;
function update() {
$.each(checks, function() {
if (this())
return false;
});
}
$(document).scroll(update);
$(window).resize(update);
setTimeout(update, 0);
$('article section').each(function() {
var section = $(this);
var anchor = $('a', section);
// Move content until next section inside section
section.append(section.nextUntil('section'));
var title = anchor.attr('title') || $('h1,h2', section).first().text();
var id = section.attr('id');
if (!id) {
id = hyphenate(title)
.replace(/\s+/g, '-')
.replace(/^#/, '')
.replace(/[!"#$%&'\()*+,.\/:;<=>?@\[\\\]\^_`{|}~]+/g, '-')
.replace(/-+/g, '-');
section.attr('id', id);
anchor.attr('name', id);
}
function activate() {
if (active)
active.removeClass('active');
selector.addClass('active');
active = selector;
}
// Create table of contents on the fly
if (toc) {
var selector = $('
').appendTo(toc);
if (section.is('.spacer'))
selector.addClass('spacer');
$('a', selector).click(function() {
smoothScrollTo(section, function() {
window.location.hash = id;
});
return false;
});
checks.push(function() {
var visible = isVisible(section);
if (visible)
activate();
return visible;
});
}
});
// Adjust height of last section so that the last anchor aligns perfectly
// with the top of the browser window.
var lastSection = $('article section:last');
var lastAnchor = $('a[name]', lastSection);
function resize() {
lastSection.height('auto');
var bottom = $(document).height() - lastAnchor.offset().top - $(window).height();
if (bottom < 0)
lastSection.height(lastSection.height() - bottom);
}
if (lastSection.length && lastAnchor.length) {
$(window).on({
load: resize,
resize: resize
});
resize();
}
};
behaviors.sticky = function() {
$('.sticky').each(function() {
me = $(this);
container = $('').append(me.contents()).appendTo(me);
// Insert a div wrapper of which the fixed class is modified depending on position
$(window).scroll(function() {
if (container.is(':visible'))
container.toggleClass('fixed', me.offset().top - $(this).scrollTop() <= 0);
});
});
};
behaviors.hash = function() {
var hash = unescape(window.location.hash);
if (hash) {
// First see if there's a class member to open
var target = $(hash);
if (target.length) {
if (target.hasClass('member'))
toggleMember(target);
smoothScrollTo(target);
}
}
};
behaviors.thumbnails = function() {
var thumbnails = $('.thumbnail');
var height = 0;
thumbnails.each(function() {
height = Math.max(height, $(this).height());
});
$('.thumbnail').height(height);
};
behaviors.expandableLists = function() {
$('.expandable-list').each(function() {
var list = $(this);
$('')
.prependTo(list)
.click(function() {
list.toggleClass('expanded');
});
});
};
behaviors.referenceClass = function() {
var classes = $('.reference-classes');
if (classes.length) {
// Mark currently selected class as active. Do it client-sided
// since the menu is generated by jsdocs.
var path = window.location.pathname.toLowerCase();
$('a[href="' + path + '"]', classes).addClass('active');
}
};
behaviors.hover = function() {
$('.hover').hover(function() {
$('.normal', this).toggleClass('hidden');
$('.over', this).toggleClass('hidden');
});
};
behaviors.code = function() {
$('.code:visible, pre:visible code').each(function() {
createCode($(this));
});
};
behaviors.paperscript = function() {
// Ignore all paperscripts in the automatic load event, and load them
// separately in createPaperScript() when needed.
$('script[type="text/paperscript"]').attr('ignore', 'true');
$('.paperscript:visible').each(function() {
createPaperScript($(this));
});
};
function createCodeMirror(place, options, source) {
return new CodeMirror(place, $.extend({}, {
mode: 'javascript',
lineNumbers: true,
matchBrackets: true,
tabSize: 4,
indentUnit: 4,
indentWithTabs: true,
tabMode: 'shift',
value: source.text().match(
// Remove first & last empty line
/^\s*?[\n\r]?([\u0000-\uffff]*?)[\n\r]?\s*?$/)[1]
}, options));
}
function createCode(element) {
if (element.data('initialized'))
return;
var start = element.attr('start');
var highlight = element.attr('highlight');
var editor = createCodeMirror(function(el) {
element.replaceWith(el);
}, {
lineNumbers: !element.parent('.resource-text').length,
firstLineNumber: parseInt(start || 1, 10),
mode: element.attr('mode') || 'javascript',
readOnly: true
}, element);
if (highlight) {
var highlights = highlight.split(',');
for (var i = 0, l = highlights.length; i < l; i++) {
var highlight = highlights[i].split('-');
var hlStart = parseInt(highlight[0], 10) - 1;
var hlEnd = highlight.length == 2
? parseInt(highlight[1], 10) - 1 : hlStart;
if (start) {
hlStart -= start - 1;
hlEnd -= start - 1;
}
for (var j = hlStart; j <= hlEnd; j++) {
editor.setLineClass(j, 'highlight');
}
}
}
element.data('initialized', true);
}
function createPaperScript(element) {
if (element.data('initialized'))
return;
var script = $('script', element).orNull(),
runButton = $('.button.run', element).orNull();
if (!script)
return;
// Now load / parse / execute the script
script.removeAttr('ignore');
var scope = paper.PaperScript.load(script[0]);
if (!runButton)
return;
var canvas = $('canvas', element),
hasResize = canvas.attr('resize'),
showSplit = element.hasClass('split'),
sourceFirst = element.hasClass('source'),
editor = null,
hasBorders = true,
edited = false,
animateExplain,
explain = $('.explain', element).orNull(),
source = $('').insertBefore(script);
if (explain) {
explain.addClass('hidden');
var text = explain.html().replace(/http:\/\/([\w.]+)/g, function(url, domain) {
return '' + domain + '';
}).trim();
// Add explanation bubbles to tickle the visitor's fancy
var explanations = [{
index: 0,
list: [
[text ? 4 : 3, text || ''],
[1, ''],
[4, 'Note: You can view and even edit the source right here in the browser'],
[1, ''],
[3, 'To do so, simply press the Source button →']
]
}, {
index: 0,
indexIfEdited: 3, // Skip first sentence if user has already edited code
list: [
[4, ''],
[3, 'Why don\'t you try editing the code?'],
[1, ''],
[4, 'To run it again, simply press press Run →']
]
}];
var timer,
mode;
animateExplain = function(clearPrevious) {
if (timer)
timer = clearTimeout(timer);
// Set previous mode's index to the end?
if (mode && clearPrevious)
mode.index = mode.list.length;
mode = explanations[source.hasClass('hidden') ? 0 : 1];
if (edited && mode.index < mode.indexIfEdited)
mode.index = mode.indexIfEdited;
var entry = mode.list[mode.index];
if (entry) {
explain.removeClass('hidden');
explain.html(entry[1]);
timer = setTimeout(function() {
// Only increase once we're stepping, not in animate()
// itself, as entering & leaving would continuosly step
mode.index++;
animateExplain();
}, entry[0] * 1000);
}
if (!entry || !entry[1])
explain.addClass('hidden');
};
element
.mouseover(function() {
if (!timer)
animateExplain();
})
.mouseout(function() {
// Check the effect of :hover on button to see if we need
// to turn off...
// TODO: make mouseenter / mouseleave events work again
if (timer && runButton.css('display') == 'none') {
timer = clearTimeout(timer);
explain.addClass('hidden');
}
});
}
function showSource(show) {
source.toggleClass('hidden', !show);
runButton.text(show ? 'Run' : 'Source');
if (explain)
animateExplain(true);
if (show && !editor) {
editor = createCodeMirror(source[0], {
onKeyEvent: function(editor, event) {
edited = true;
}
}, script);
}
}
function runScript() {
// Update script to edited version
var code = editor.getValue();
script.text(code);
// Keep a reference to the used canvas, since we're going to
// fully clear the scope and initialize again with this canvas.
// Support both old and new versions of paper.js for now:
var element = scope.view.element;
// Clear scope first, then evaluate a new script.
scope.clear();
scope.initialize(script[0]);
scope.setup(element);
scope.execute(code);
}
function resize() {
source
.width(element.width() - (hasBorders ? 2 : 1))
.height(element.height() - (hasBorders ? 2 : 0));
if (editor)
editor.refresh();
}
function toggleView() {
var show = source.hasClass('hidden');
resize();
canvas.toggleClass('hidden', show);
showSource(show);
if (!show)
runScript();
// Add extra margin if there is scrolling
var scrollHeight = $('.CodeMirror .CodeMirror-scroll', source).height();
runButton.css('margin-right', scrollHeight > element.height() ? 25 : 8);
}
if (hasResize) {
paper.view.on('resize', resize);
hasBorders = false;
source.css('border-width', '0 0 0 1px');
}
if (showSplit) {
showSource(true);
} else if (sourceFirst) {
toggleView();
}
runButton
.click(function() {
if (showSplit) {
runScript();
} else {
toggleView();
}
return false;
})
.mousedown(function() {
return false;
});
element.data('initialized', true);
}
// Reference (before behaviors)
var lastMember = null;
function toggleMember(member, dontScroll, offsetElement) {
var link = $('.member-link:first', member);
if (!link.length)
return true;
var desc = $('.member-description', member);
var visible = !link.hasClass('hidden');
// Retrieve y-offset before any changes, so we can correct scrolling after
var offset = (offsetElement || member).offset().top;
if (lastMember && !lastMember.is(member)) {
var prev = lastMember;
lastMember = null;
toggleMember(prev, true);
}
lastMember = visible && member;
link.toggleClass('hidden', visible);
desc.toggleClass('hidden', !visible);
if (!dontScroll) {
// Correct scrolling relatively to where we are, by checking the amount
// the element has shifted due to the above toggleMember call, and
// correcting by 11px offset, caused by 1px border and 10px padding.
var scroll = $(document).scrollTop();
// Only change hash if we're not in frames, since there are redrawing
// issues with that on Chrome.
if (parent === self)
window.location.hash = visible ? member.attr('id') : '';
$(document).scrollTop(scroll
+ (visible ? desc : link).offset().top - offset
+ 11 * (visible ? 1 : -1));
}
if (!member.data('initialized') && visible) {
behaviors.code();
behaviors.paperscript();
member.data('initialized', true);
}
return false;
}
$(function() {
$('.reference .member').each(function() {
var member = $(this);
var link = $('.member-link', member);
// Add header to description, with link and closing button
var header = $('').prependTo($('.member-description', member));
// Clone link, but remove name, id and change href
link.clone().removeAttr('name').removeAttr('id').attr('href', '#').appendTo(header);
// Add closing button.
header.append('');
});
// Give open / close buttons behavior
$('.reference')
.on('click', '.member-link, .member-close', function() {
return toggleMember($(this).parents('.member'));
})
.on('click', '.member-text a', function() {
if (this.href.match(/^(.*?)\/?#|$/)[1] === document.location.href.match(/^(.*?)\/?(?:#|$)/)[1]) {
toggleMember($(this.href.match(/(#.*)$/)[1]), false, $(this));
return false;
}
});
});
// DOM-Ready
$(function() {
for (var i in behaviors)
behaviors[i]();
});