diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000000..01bef8f23b8
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,22 @@
+# 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
diff --git a/README.md b/README.md
index 57624945941..91eb2397785 100644
--- a/README.md
+++ b/README.md
@@ -3,18 +3,18 @@ Slate
[](https://travis-ci.org/tripit/slate) [](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.
-
+
*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)
diff --git a/config.rb b/config.rb
index 31e4d2ca373..c69665926c8 100644
--- a/config.rb
+++ b/config.rb
@@ -1,3 +1,5 @@
+require './lib/redcarpet_header_fix'
+
set :css_dir, 'stylesheets'
set :js_dir, 'javascripts'
@@ -34,3 +36,4 @@
# Or use a different image path
# set :http_prefix, "/Content/images/"
end
+
diff --git a/lib/redcarpet_header_fix.rb b/lib/redcarpet_header_fix.rb
new file mode 100644
index 00000000000..170d4b843d7
--- /dev/null
+++ b/lib/redcarpet_header_fix.rb
@@ -0,0 +1,9 @@
+module RedcarpetHeaderFix
+ def header(text, level, id)
+ clean_id = id.gsub(/[\.]/, '-').gsub(/[^a-zA-Z0-9\-_]/, '')
+ "#{text}"
+ end
+end
+
+require 'middleman-core/renderers/redcarpet'
+Middleman::Renderers::MiddlemanRedcarpetHTML.send :include, RedcarpetHeaderFix
diff --git a/source/images/navbar.png b/source/images/navbar.png
new file mode 100644
index 00000000000..32e70e14fb4
Binary files /dev/null and b/source/images/navbar.png differ
diff --git a/source/javascripts/all_nosearch.js b/source/javascripts/all_nosearch.js
new file mode 100644
index 00000000000..4610cabe62d
--- /dev/null
+++ b/source/javascripts/all_nosearch.js
@@ -0,0 +1,4 @@
+//= require_tree ./lib
+//= require_tree ./app
+//= stub ./app/search.js
+//= stub ./lib/lunr.js
diff --git a/source/javascripts/app/lang.js b/source/javascripts/app/lang.js
index 3b02e093f4f..fdb899716c8 100644
--- a/source/javascripts/app/lang.js
+++ b/source/javascripts/app/lang.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);
diff --git a/source/javascripts/app/search.js b/source/javascripts/app/search.js
index 15213047e9d..cb81989dc28 100644
--- a/source/javascripts/app/search.js
+++ b/source/javascripts/app/search.js
@@ -1,10 +1,10 @@
(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 = $('');
-
- 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("" + $('#'+result.ref).text() + "");
});
-
- // 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('No Results Found for "' + this.value + '"');
}
} 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);
}
diff --git a/source/javascripts/app/toc.js b/source/javascripts/app/toc.js
index 3761c2bf38a..779e37e7655 100644
--- a/source/javascripts/app/toc.js
+++ b/source/javascripts/app/toc.js
@@ -1,14 +1,12 @@
(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);
diff --git a/source/javascripts/lib/energize.js b/source/javascripts/lib/energize.js
new file mode 100644
index 00000000000..6798f3c03f3
--- /dev/null
+++ b/source/javascripts/lib/energize.js
@@ -0,0 +1,169 @@
+/**
+ * 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
diff --git a/source/javascripts/lib/jquery.tocify.js b/source/javascripts/lib/jquery.tocify.js
index d122048c8d6..f791bf86246 100644
--- a/source/javascripts/lib/jquery.tocify.js
+++ b/source/javascripts/lib/jquery.tocify.js
@@ -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;
diff --git a/source/layouts/layout.erb b/source/layouts/layout.erb
index 1ea9cf1a187..915505b0405 100644
--- a/source/layouts/layout.erb
+++ b/source/layouts/layout.erb
@@ -19,12 +19,17 @@ under the License.
+
<%= current_page.data.title || "API Documentation" %>
<%= stylesheet_link_tag :screen, media: :screen %>
<%= stylesheet_link_tag :print, media: :print %>
- <%= javascript_include_tag "all" %>
+ <% if current_page.data.search %>
+ <%= javascript_include_tag "all" %>
+ <% else %>
+ <%= javascript_include_tag "all_nosearch" %>
+ <% end %>
<% if language_tabs %>