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 [![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. -Screenshot of Example Documentation created with Slate +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) 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 %>