Commit c5381c62 authored by Tariq Islam's avatar Tariq Islam

Merge pull request #90 from tripit/dev

Version 1.0
parents 0a8c73ad 60d1dbd3
# Changelog
## Version 1.0
*July 2, 2014*
[View Issues](https://github.com/tripit/slate/issues?milestone=1&state=closed)
**Features:**
- Responsive designs for phones and tablets
- Started tagging versions
**Fixes:**
- Fixed 'unrecognized expression' error
- Fixed #undefined hash bug
- Fixed bug where the current language tab would be unselected
- Fixed bug where tocify wouldn't highlight the current section while searching
- Fixed bug where ids of header tags would have special characters that caused problems
- Updated layout so that pages with disabled search wouldn't load search.js
- Cleaned up Javascript
......@@ -3,18 +3,18 @@ Slate
[![Build Status](https://travis-ci.org/tripit/slate.svg?branch=master)](https://travis-ci.org/tripit/slate) [![Dependency Status](https://gemnasium.com/tripit/slate.png)](https://gemnasium.com/tripit/slate)
Slate helps you create beautiful single-page API documentation. Think of it as an intelligent, modern documentation template for your API.
Slate helps you create beautiful API documentation. Think of it as an intelligent, responsive documentation template for your API.
<img src="https://dl.dropboxusercontent.com/u/95847291/github%20images/slate/slate_screenshot.png" width=700 alt="Screenshot of Example Documentation created with Slate">
<img src="https://dl.dropboxusercontent.com/u/95847291/github%20images/slate/slate_screenshot_new.png" width=700 alt="Screenshot of Example Documentation created with Slate">
*The example above was created with Slate. Check it out at [tripit.github.io/slate](http://tripit.github.io/slate).*
Features
------------
* **Clean, intuitive design** — with Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. In addition to the design you see on screen, Slate comes with a print stylesheet, so your docs look great on paper.
* **Clean, intuitive design** — with Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even print.
* **Everything on a single page** — gone are the days where your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so it's insanely easy to link to a particular point in the documentation.
* **Everything on a single page** — gone are the days where your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy.
* **Slate is just Markdown** — when you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks!
......@@ -55,6 +55,8 @@ Now that Slate is all set up your machine, you'll probably want to learn more ab
Examples of Slate in the Wild
---------------------------------
* [Travis-CI's API docs](http://docs.travis-ci.com/api/)
* [Mozilla's localForage docs](http://mozilla.github.io/localForage/)
* [Orchestrate.io API docs](https://docs.orchestrate.io/)
* [ChaiOne Gameplan API docs](http://chaione.github.io/gameplanb2b/#introduction)
* [Drcaban's Build a Quine tutorial](http://drcabana.github.io/build-a-quine/#introduction)
......@@ -69,6 +71,18 @@ Need Help? Found a bug?
Just [submit a issue](https://github.com/tripit/slate/issues) to the Slate Github if you need any help. And, of course, feel free to submit pull requests with bug fixes or changes.
Contributors
--------------------
Slate was built by [Robert Lord](http://lord.io) while at [TripIt](http://tripit.com).
Thanks to the following people who have submitted major pull requests:
- [@chrissrogers](https://github.com/chrissrogers)
- [@bootstraponline](https://github.com/bootstraponline)
Also, thanks to [Sauce Labs](http://saucelabs.com) for helping sponsor the project.
Special Thanks
--------------------
- [Middleman](https://github.com/middleman/middleman)
......@@ -76,11 +90,3 @@ Special Thanks
- [middleman-syntax](https://github.com/middleman/middleman-syntax)
- [middleman-gh-pages](https://github.com/neo/middleman-gh-pages)
- [Font Awesome](http://fortawesome.github.io/Font-Awesome/)
Contributors
--------------------
Thanks to the following people who have submitted pull requests:
- [@chrissrogers](https://github.com/chrissrogers)
- [@bootstraponline](https://github.com/bootstraponline)
require './lib/redcarpet_header_fix'
set :css_dir, 'stylesheets'
set :js_dir, 'javascripts'
......@@ -34,3 +36,4 @@ configure :build do
# Or use a different image path
# set :http_prefix, "/Content/images/"
end
module RedcarpetHeaderFix
def header(text, level, id)
clean_id = id.gsub(/[\.]/, '-').gsub(/[^a-zA-Z0-9\-_]/, '')
"<h#{level} id='#{clean_id}'>#{text}</h1>"
end
end
require 'middleman-core/renderers/redcarpet'
Middleman::Renderers::MiddlemanRedcarpetHTML.send :include, RedcarpetHeaderFix
//= require_tree ./lib
//= require_tree ./app
//= stub ./app/search.js
//= stub ./lib/lunr.js
......@@ -21,9 +21,10 @@ under the License.
function activateLanguage(language) {
if (!language) return;
if (language === "") return;
$("#lang-selector a").removeClass('active');
$("#lang-selector a[data-language-name='" + language + "']").addClass('active');
$(".lang-selector a").removeClass('active');
$(".lang-selector a[data-language-name='" + language + "']").addClass('active');
for (var i=0; i < languages.length; i++) {
$(".highlight." + languages[i]).hide();
}
......@@ -41,6 +42,9 @@ under the License.
hash = hash.replace(/^#+/, '');
}
history.pushState({}, '', '?' + language + '#' + hash);
// save language as next default
localStorage.setItem("language", language);
}
function setupLanguages(l) {
......@@ -53,7 +57,6 @@ under the License.
// the language is in the URL, so use that language!
activateLanguage(location.search.substr(1));
// set this language as the default for next time, if the URL has no language
localStorage.setItem("language", location.search.substr(1));
} else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) {
// the language was the last selected one saved in localstorage, so use that language!
......@@ -66,7 +69,7 @@ under the License.
// if we click on a language tab, activate that language
$(function() {
$("#lang-selector a").on("click", function() {
$(".lang-selector a").on("click", function() {
var language = $(this).data("language-name");
pushURL(language);
activateLanguage(language);
......
(function (global) {
var $global = $(global);
var content, darkBox, searchInfo;
var content, darkBox, searchResults;
var highlightOpts = { element: 'span', className: 'search-highlight' };
var index = new lunr.Index;
var index = new lunr.Index();
index.ref('id');
index.field('title', { boost: 10 });
......@@ -14,15 +14,10 @@
$(populate);
$(bind);
function populate () {
$('h1').each(function () {
function populate() {
$('h1, h2').each(function() {
var title = $(this);
var body = title.nextUntil('h1');
var wrapper = $('<section id="section-' + title.prop('id') + '"></section>');
title.after(wrapper.append(body));
wrapper.prepend(title);
var body = title.nextUntil('h1, h2');
index.add({
id: title.prop('id'),
title: title.text(),
......@@ -31,99 +26,46 @@
});
}
function bind () {
function bind() {
content = $('.content');
darkBox = $('.dark-box');
searchInfo = $('.search-info');
$('#input-search')
.on('keyup', search)
.on('focus', active)
.on('blur', inactive);
}
function refToHeader (itemRef) {
return $('.tocify-item[data-unique=' + itemRef + ']').closest('.tocify-header');
}
searchResults = $('.search-results');
function sortDescending (obj2, obj1) {
var s1 = parseInt(obj1.id.replace(/[^\d]/g, ''), 10);
var s2 = parseInt(obj2.id.replace(/[^\d]/g, ''), 10);
return s1 === s2 ? 0 : s1 < s2 ? -1 : 1;
$('#input-search').on('keyup', search);
}
function resetHeaderLocations () {
var headers = $(".tocify-header").sort(sortDescending);
$.each(headers, function (index, item) {
$(item).insertBefore($("#toc ul:first-child"));
});
}
function search (event) {
var sections = $('section, #toc .tocify-header');
searchInfo.hide();
function search(event) {
unhighlight();
searchResults.addClass('visible');
// ESC clears the field
if (event.keyCode === 27) this.value = '';
if (this.value) {
sections.hide();
// results are sorted by score in descending order
var results = index.search(this.value);
var results = index.search(this.value).filter(function(r) {
return r.score > 0.0001;
});
if (results.length) {
resetHeaderLocations();
var lastRef;
$.each(results, function (index, item) {
if (item.score <= 0.0001) return; // remove low-score results
var itemRef = item.ref;
$('#section-' + itemRef).show();
// headers must be repositioned in the DOM
var closestHeader = refToHeader(itemRef);
if (lastRef) {
refToHeader(lastRef).insertBefore(closestHeader);
}
closestHeader.show();
lastRef = itemRef;
searchResults.empty();
$.each(results, function (index, result) {
searchResults.append("<li><a href='#" + result.ref + "'>" + $('#'+result.ref).text() + "</a></li>");
});
// position first element. it wasn't positioned above if len > 1
if (results.length > 1) {
var firstRef = results[0].ref;
var secondRef = results[1].ref
refToHeader(firstRef).insertBefore(refToHeader(secondRef));
}
highlight.call(this);
} else {
sections.show();
searchInfo.text('No Results Found for "' + this.value + '"').show();
searchResults.html('<li>No Results Found for "' + this.value + '"</li>');
}
} else {
sections.show();
unhighlight();
searchResults.removeClass('visible');
}
// HACK trigger tocify height recalculation
$global.triggerHandler('scroll.tocify');
$global.triggerHandler('resize');
}
function active () {
search.call(this, {});
}
function inactive () {
unhighlight();
searchInfo.hide();
}
function highlight () {
function highlight() {
if (this.value) content.highlight(this.value, highlightOpts);
}
function unhighlight () {
function unhighlight() {
content.unhighlight(highlightOpts);
}
......
(function (global) {
var toc;
var closeToc = function() {
$(".tocify-wrapper").removeClass('open');
$("#nav-button").removeClass('open');
};
global.toc = toc;
$(toc);
$(animate);
function toc () {
toc = $("#toc").tocify({
var makeToc = function() {
global.toc = $("#toc").tocify({
selectors: 'h1, h2',
extendPage: false,
theme: 'none',
......@@ -17,13 +15,22 @@
hideEffectSpeed: 180,
ignoreSelector: '.toc-ignore',
highlightOffset: 60,
scrollTo: -2,
scrollTo: -1,
scrollHistory: true,
hashGenerator: function (text, element) {
return element.prop('id');
}
}).data('toc-tocify');
}
$("#nav-button").click(function() {
$(".tocify-wrapper").toggleClass('open');
$("#nav-button").toggleClass('open');
return false;
});
$(".page-wrapper").click(closeToc);
$(".tocify-item").click(closeToc);
};
// Hack to make already open sections to start opened,
// instead of displaying an ugly animation
......@@ -33,5 +40,8 @@
}, 50);
}
$(makeToc);
$(animate);
})(window);
/**
* energize.js v0.1.0
*
* Speeds up click events on mobile devices.
* https://github.com/davidcalhoun/energize.js
*/
(function() { // Sandbox
/**
* Don't add to non-touch devices, which don't need to be sped up
*/
if(!('ontouchstart' in window)) return;
var lastClick = {},
isThresholdReached, touchstart, touchmove, touchend,
click, closest;
/**
* isThresholdReached
*
* Compare touchstart with touchend xy coordinates,
* and only fire simulated click event if the coordinates
* are nearby. (don't want clicking to be confused with a swipe)
*/
isThresholdReached = function(startXY, xy) {
return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5;
};
/**
* touchstart
*
* Save xy coordinates when the user starts touching the screen
*/
touchstart = function(e) {
this.startXY = [e.touches[0].clientX, e.touches[0].clientY];
this.threshold = false;
};
/**
* touchmove
*
* Check if the user is scrolling past the threshold.
* Have to check here because touchend will not always fire
* on some tested devices (Kindle Fire?)
*/
touchmove = function(e) {
// NOOP if the threshold has already been reached
if(this.threshold) return false;
this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]);
};
/**
* touchend
*
* If the user didn't scroll past the threshold between
* touchstart and touchend, fire a simulated click.
*
* (This will fire before a native click)
*/
touchend = function(e) {
// Don't fire a click if the user scrolled past the threshold
if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) {
return;
}
/**
* Create and fire a click event on the target element
* https://developer.mozilla.org/en/DOM/event.initMouseEvent
*/
var touch = e.changedTouches[0],
evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
evt.simulated = true; // distinguish from a normal (nonsimulated) click
e.target.dispatchEvent(evt);
};
/**
* click
*
* Because we've already fired a click event in touchend,
* we need to listed for all native click events here
* and suppress them as necessary.
*/
click = function(e) {
/**
* Prevent ghost clicks by only allowing clicks we created
* in the click event we fired (look for e.simulated)
*/
var time = Date.now(),
timeDiff = time - lastClick.time,
x = e.clientX,
y = e.clientY,
xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)],
target = closest(e.target, 'A') || e.target, // needed for standalone apps
nodeName = target.nodeName,
isLink = nodeName === 'A',
standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href");
lastClick.time = time;
lastClick.x = x;
lastClick.y = y;
/**
* Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire),
* so we have to add more logic to determine the time of the last click. Not perfect...
*
* Older, simpler check: if((!e.simulated) || standAlone)
*/
if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) {
e.preventDefault();
e.stopPropagation();
if(!standAlone) return false;
}
/**
* Special logic for standalone web apps
* See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window
*/
if(standAlone) {
window.location = target.getAttribute("href");
}
/**
* Add an energize-focus class to the targeted link (mimics :focus behavior)
* TODO: test and/or remove? Does this work?
*/
if(!target || !target.classList) return;
target.classList.add("energize-focus");
window.setTimeout(function(){
target.classList.remove("energize-focus");
}, 150);
};
/**
* closest
* @param {HTMLElement} node current node to start searching from.
* @param {string} tagName the (uppercase) name of the tag you're looking for.
*
* Find the closest ancestor tag of a given node.
*
* Starts at node and goes up the DOM tree looking for a
* matching nodeName, continuing until hitting document.body
*/
closest = function(node, tagName){
var curNode = node;
while(curNode !== document.body) { // go up the dom until we find the tag we're after
if(!curNode || curNode.nodeName === tagName) { return curNode; } // found
curNode = curNode.parentNode; // not found, so keep going up
}
return null; // not found
};
/**
* Add all delegated event listeners
*
* All the events we care about bubble up to document,
* so we can take advantage of event delegation.
*
* Note: no need to wait for DOMContentLoaded here
*/
document.addEventListener('touchstart', touchstart, false);
document.addEventListener('touchmove', touchmove, false);
document.addEventListener('touchend', touchend, false);
document.addEventListener('click', click, true); // TODO: why does this use capture?
})();
\ No newline at end of file
......@@ -681,9 +681,11 @@
self.calculateHeights();
}
var scrollTop = $(window).scrollTop();
// Determines the index of the closest anchor
self.cachedAnchors.each(function(idx) {
if (self.cachedHeights[idx] - $(window).scrollTop() < 0) {
if (self.cachedHeights[idx] - scrollTop < 0) {
closestAnchorIdx = idx;
} else {
return false;
......@@ -696,7 +698,7 @@
elem = $('li[data-unique="' + anchorText + '"]');
// If the `highlightOnScroll` option is true and a next element is found
if(self.options.highlightOnScroll && elem.length) {
if(self.options.highlightOnScroll && elem.length && !elem.hasClass(self.focusClass)) {
// Removes highlighting from all of the list item's
self.element.find("." + self.focusClass).removeClass(self.focusClass);
......@@ -722,12 +724,14 @@
if(self.options.scrollHistory) {
if(window.location.hash !== "#" + anchorText) {
// IF STATEMENT ADDED BY ROBERT
if(window.location.hash !== "#" + anchorText && anchorText !== undefined) {
if(history.replaceState) {
if(history.replaceState) {
history.replaceState({}, "", "#" + anchorText);
// provide a fallback
} else {
} else {
scrollV = document.body.scrollTop;
scrollH = document.body.scrollLeft;
location.hash = "#" + anchorText;
......
......@@ -19,12 +19,17 @@ under the License.
<head>
<meta charset="utf-8">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title><%= current_page.data.title || "API Documentation" %></title>
<%= stylesheet_link_tag :screen, media: :screen %>
<%= stylesheet_link_tag :print, media: :print %>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<%= javascript_include_tag "all" %>
<% if current_page.data.search %>
<%= javascript_include_tag "all" %>
<% else %>
<%= javascript_include_tag "all_nosearch" %>
<% end %>
<% if language_tabs %>
<script>
......@@ -36,13 +41,30 @@ under the License.
</head>
<body class="<%= page_classes %>">
<a href="#" id="nav-button">
<span>
NAV
<%= image_tag('navbar.png') %>
</span>
</a>
<div class="tocify-wrapper">
<%= image_tag "logo.png" %>
<% if language_tabs %>
<div class="lang-selector">
<% language_tabs.each do |lang| %>
<% if lang.is_a? Hash %>
<a href="#" data-language-name="<%= lang.keys.first %>"><%= lang.values.first %></a>
<% else %>
<a href="#" data-language-name="<%= lang %>"><%= lang %></a>
<% end %>
<% end %>
</div>
<% end %>
<% if current_page.data.search %>
<div class="search">
<input type="text" class="search" id="input-search">
<input type="text" class="search" id="input-search" placeholder="Search">
</div>
<div class="search-info"></div>
<ul class="search-results"></ul>
<% end %>
<div id="toc">
</div>
......@@ -64,7 +86,7 @@ under the License.
</div>
<div class="dark-box">
<% if language_tabs %>
<div id="lang-selector">
<div class="lang-selector">
<% language_tabs.each do |lang| %>
<% if lang.is_a? Hash %>
<a href="#" data-language-name="<%= lang.keys.first %>"><%= lang.values.first %></a>
......
......@@ -28,7 +28,7 @@ body {
@extend %default-font;
}
.tocify, .toc-footer {
.tocify, .toc-footer, .lang-selector, .search, #nav-button {
display: none;
}
......
This diff is collapsed.
......@@ -57,9 +57,12 @@ $examples-width: 50%; // portion of the screen taken up by code examples
$logo-margin: 20px; // margin between nav items and logo, ignored if search is active
$main-padding: 28px; // padding to left and right of content & examples
$nav-padding: 15px; // padding to left and right of navbar
$nav-v-padding: 10px; // padding used vertically around search boxes and results
$nav-indent: 10px; // extra padding for ToC subitems
$code-annotation-padding: 13px; // padding inside code annotations
$h1-margin-bottom: 21px; // padding under the largest header tags
$tablet-width: 930px; // min width before reverting to tablet size
$phone-width: $tablet-width - $nav-width; // min width before reverting to mobile size
// FONTS
......@@ -84,8 +87,8 @@ $h1-margin-bottom: 21px; // padding under the largest header tags
////////////////////
$nav-active-shadow: #000;
$nav-footer-border-color: #666;
$nav-embossed-border-top: 1px solid #000;
$nav-embossed-border-bottom: 1px solid #404040;
$nav-embossed-border-top: #000;
$nav-embossed-border-bottom: #939393;
$main-embossed-text-shadow: 0px 1px 0px #fff;
$search-box-border-color: #666;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment