├── .gitignore ├── app ├── img │ ├── thumbnails │ │ └── .gitkeep │ └── blank.png ├── fonts │ └── lib │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.svg ├── css │ ├── views.show.css │ ├── app.css │ └── views.index.css ├── js │ ├── controllers.show.js │ ├── app.js │ ├── framework.js │ ├── controllers.index.js │ └── lib │ │ ├── underscore.js │ │ └── bootstrap.js └── index.html ├── .rspec ├── Gemfile ├── Rakefile ├── lib ├── storys │ ├── version.rb │ ├── core_ext │ │ └── pathname.rb │ ├── story.rb │ ├── update.rb │ └── package.rb └── storys.rb ├── bin └── storys ├── spec ├── factories.rb ├── spec_helper.rb └── story_spec.rb ├── LICENSE ├── storys.gemspec └── Gemfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg/ 2 | -------------------------------------------------------------------------------- /app/img/thumbnails/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | -------------------------------------------------------------------------------- /lib/storys/version.rb: -------------------------------------------------------------------------------- 1 | module Storys 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /app/img/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/storys/master/app/img/blank.png -------------------------------------------------------------------------------- /app/fonts/lib/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/storys/master/app/fonts/lib/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/fonts/lib/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/storys/master/app/fonts/lib/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/fonts/lib/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/storys/master/app/fonts/lib/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /bin/storys: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "storys" 4 | 5 | root_path = Pathname.new(ARGV.first || ".").realpath 6 | 7 | storys = Storys::Package.new(root_path) 8 | storys.update 9 | -------------------------------------------------------------------------------- /app/css/views.show.css: -------------------------------------------------------------------------------- 1 | #view-show { 2 | display: none; 3 | } 4 | 5 | iframe { 6 | display: none; 7 | } 8 | 9 | #view-show #story { 10 | position: relative; 11 | } 12 | 13 | #view-show #return-to-index { 14 | padding: 5px; 15 | } 16 | 17 | #view-show #return-to-index a { 18 | color: #ffffff; 19 | } 20 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :package, class: 'Storys::Package' do 3 | path { Pathname.new(".") } 4 | initialize_with { new(path) } 5 | end 6 | 7 | factory :story, class: 'Storys::Story' do 8 | path { Pathname.new(".") } 9 | initialize_with { new(build(:package), path) } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/storys.rb: -------------------------------------------------------------------------------- 1 | #Ruby stdlib 2 | require "pathname" 3 | require "fileutils" 4 | require "cgi" 5 | require "uri" 6 | require "json" 7 | require "digest" 8 | 9 | #Gems 10 | require "nsf" 11 | require "addressable/uri" 12 | require "naturally" 13 | 14 | #Core Extensions 15 | require "storys/core_ext/pathname" 16 | 17 | module Storys 18 | end 19 | 20 | require "storys/package" 21 | require "storys/update" 22 | require "storys/story" 23 | -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | body, div, a, img { 2 | -webkit-user-select: none; 3 | -moz-user-select: none; 4 | -khtml-user-select: none; 5 | -ms-user-select: none; 6 | user-select: none; 7 | } 8 | 9 | h1, p, ul { margin-top: 0; margin-bottom: 1.2em; } 10 | h1 { margin-top: 0; margin-bottom: 0.6em; text-align: left; } 11 | h2 { margin-top: 0; margin-bottom: 0.6em; text-align: left; } 12 | h3 { margin-top: 0; margin-bottom: 0.6em; text-align: left; } 13 | h4 { margin-top: 0; margin-bottom: 0.6em; text-align: left; } 14 | 15 | .clear { clear: both; } 16 | 17 | #wrapper { 18 | max-width: 1200px; 19 | margin: 51px auto 0 auto; 20 | padding: 30px; 21 | } 22 | 23 | #navbar-content-target { 24 | text-align: center; 25 | } 26 | 27 | .navbar-nav.navbar-right:last-child { 28 | margin-right: 0 !important; 29 | } 30 | -------------------------------------------------------------------------------- /lib/storys/core_ext/pathname.rb: -------------------------------------------------------------------------------- 1 | class Pathname 2 | def descendant_files 3 | out = children.select { |p| p.html? && !p.hidden? } 4 | children.select { |p| p.directory? && !p.hidden? }.each do |p| 5 | out += p.descendant_files 6 | end 7 | out 8 | end 9 | 10 | def html? 11 | file? && %w(.html).include?(extname) 12 | end 13 | 14 | def hidden? 15 | basename.to_s[0..0] == "." 16 | end 17 | 18 | def update_ext(extension) 19 | return self if extname == extension 20 | Pathname.new("#{to_s}#{extension}") 21 | end 22 | 23 | def write(content, options = {}) 24 | preserve_mtime = options.delete(:preserve_mtime) 25 | _atime, _mtime = atime, mtime if preserve_mtime 26 | 27 | open("w", options) { |file| file << content } 28 | 29 | utime(_atime, _mtime) if preserve_mtime 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # Require this file using `require "spec_helper"` to ensure that it is only 4 | # loaded once. 5 | # 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | 8 | require "bundler/setup" 9 | Bundler.require(:default, :development) 10 | 11 | require "storys" 12 | 13 | RSpec.configure do |config| 14 | config.treat_symbols_as_metadata_keys_with_true_values = true 15 | config.run_all_when_everything_filtered = true 16 | config.filter_run :focus 17 | 18 | # Run specs in random order to surface order dependencies. If you find an 19 | # order dependency and want to debug it, you can fix the order by providing 20 | # the seed, which is printed after each run. 21 | # --seed 1234 22 | config.order = 'random' 23 | 24 | config.include FactoryGirl::Syntax::Methods 25 | end 26 | 27 | require_relative "factories.rb" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Brenton Fletcher (http://blog.bloople.net i@bloople.net) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /app/js/controllers.show.js: -------------------------------------------------------------------------------- 1 | controllers.show = function(key) { 2 | var _this = this; 3 | 4 | var story = _.find(store, function(story) { 5 | return story.key == key; 6 | }); 7 | 8 | function loadStory(url) { 9 | var iframe = $(""); 10 | $("body").append(iframe); 11 | iframe.load(function() { 12 | $("#story").html(iframe[0].contentDocument.body.innerHTML); 13 | iframe.remove(); 14 | }); 15 | iframe.attr("src", url); 16 | } 17 | 18 | function setVisited(key) { 19 | if(!localStorage["visited." + key]) localStorage["visited." + key] = "0"; 20 | localStorage["visited." + key] = parseInt(localStorage["visited." + key]) + 1; 21 | } 22 | 23 | this.init = function() { 24 | console.log("starting show"); 25 | $("#view-show").show().addClass("current-view"); 26 | loadStory(story.url); 27 | setVisited(story.key); 28 | } 29 | 30 | this.render = function() { 31 | } 32 | 33 | this.destroy = function() { 34 | console.log("destroying show"); 35 | $("#story").empty(); 36 | $("#view-show").hide().removeClass("current-view"); 37 | } 38 | } 39 | 40 | controllers.show.setup = function() { 41 | }; 42 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | var store = null; 2 | if(!localStorage["visited"]) localStorage["visited"] = {}; 3 | 4 | $(function() { 5 | $(document).on("dragstart", "a, img", false); 6 | 7 | $(window).bind('hashchange', function() { 8 | $(window).scrollTop((utils.page() - 1) * Math.max(0, $(window).height() - 51 - 60)); 9 | }).trigger('hashchange'); 10 | 11 | $("#page-back").click(function(e) { 12 | e.stopPropagation(); 13 | utils.page(utils.page() - 1); 14 | }); 15 | $("#page-back-10").click(function(e) { 16 | e.stopPropagation(); 17 | utils.page(utils.page() - 10); 18 | }); 19 | $("#page-next").click(function(e) { 20 | e.stopPropagation(); 21 | utils.page(utils.page() + 1); 22 | }); 23 | $("#page-next-10").click(function(e) { 24 | e.stopPropagation(); 25 | utils.page(utils.page() + 10); 26 | }); 27 | $("#page-home").click(function(e) { 28 | e.stopPropagation(); 29 | location.hash = lastControllerLocation; 30 | }); 31 | 32 | $.getJSON("data.json").done(function(data) { 33 | if(data.length == 0) alert("No data.json, or data invalid."); 34 | 35 | store = data; 36 | 37 | window.router = new router(); 38 | router.init(); 39 | if(location.hash == "#" || location.hash == "") location.hash = "#index!1"; 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /storys.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path("../lib/storys/version", __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "storys" 6 | s.version = Storys::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = ['Brenton "B-Train" Fletcher'] 9 | s.email = ["i@bloople.net"] 10 | s.homepage = "http://github.com/bloopletech/storys" 11 | s.summary = "Storys indexes a collection of stories and generates a SPA that browses the collection." 12 | s.description = "A collection of stories is a directory (the container) containing 1..* HTML files (stories). Storys indexes a collection in this format, and generates a HTML/JS Single Page Application (SPA) that allows you to browse and view stories in your collection. The SPA UI is much easier to use than using a filesystem browser; and the SPA, along with the collection can be trivially served over a network by putting the collection and the SPA in a directory served by nginx or Apache." 13 | 14 | s.required_rubygems_version = ">= 1.3.6" 15 | s.rubyforge_project = "storys" 16 | 17 | s.add_development_dependency "bundler", ">= 1.0.0" 18 | s.add_development_dependency "rspec", ">= 2.14.1" 19 | s.add_development_dependency "factory_girl", ">= 4.4.0" 20 | s.add_dependency "nsf" 21 | s.add_dependency "addressable", ">= 2.3.5" 22 | s.add_dependency "naturally", ">= 1.0.3" 23 | 24 | s.files = `git ls-files`.split("\n") 25 | s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact 26 | s.require_path = 'lib' 27 | end 28 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | storys (0.0.5) 5 | addressable (>= 2.3.5) 6 | naturally (>= 1.0.3) 7 | nsf 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | Ascii85 (1.0.2) 13 | activesupport (4.0.3) 14 | i18n (~> 0.6, >= 0.6.4) 15 | minitest (~> 4.2) 16 | multi_json (~> 1.3) 17 | thread_safe (~> 0.1) 18 | tzinfo (~> 0.3.37) 19 | addressable (2.3.5) 20 | afm (0.2.0) 21 | atomic (1.1.15) 22 | clbustos-rtf (0.5.0) 23 | diff-lcs (1.2.5) 24 | factory_girl (4.4.0) 25 | activesupport (>= 3.0.0) 26 | hashery (2.1.1) 27 | i18n (0.6.9) 28 | mini_portile (0.5.1) 29 | minitest (4.7.5) 30 | multi_json (1.8.4) 31 | naturally (1.1.0) 32 | nokogiri (1.6.0) 33 | mini_portile (~> 0.5.0) 34 | nsf (0.0.10) 35 | clbustos-rtf (>= 0.1.0) 36 | nokogiri (>= 1.4.4) 37 | prawn (>= 0.0.0) 38 | ruby-rtf (>= 0.0.0) 39 | pdf-reader (1.3.3) 40 | Ascii85 (~> 1.0.0) 41 | afm (~> 0.2.0) 42 | hashery (~> 2.0) 43 | ruby-rc4 44 | ttfunk 45 | prawn (0.12.0) 46 | pdf-reader (>= 0.9.0) 47 | ttfunk (~> 1.0.2) 48 | rspec (2.14.1) 49 | rspec-core (~> 2.14.0) 50 | rspec-expectations (~> 2.14.0) 51 | rspec-mocks (~> 2.14.0) 52 | rspec-core (2.14.8) 53 | rspec-expectations (2.14.5) 54 | diff-lcs (>= 1.1.3, < 2.0) 55 | rspec-mocks (2.14.6) 56 | ruby-rc4 (0.1.5) 57 | ruby-rtf (0.0.1) 58 | thread_safe (0.2.0) 59 | atomic (>= 1.1.7, < 2) 60 | ttfunk (1.0.3) 61 | tzinfo (0.3.38) 62 | 63 | PLATFORMS 64 | ruby 65 | 66 | DEPENDENCIES 67 | bundler (>= 1.0.0) 68 | factory_girl (>= 4.4.0) 69 | rspec (>= 2.14.1) 70 | storys! 71 | -------------------------------------------------------------------------------- /app/css/views.index.css: -------------------------------------------------------------------------------- 1 | #view-index { 2 | display: none; 3 | } 4 | 5 | #header { margin: 5px 5px 24px 5px; } 6 | #search { width: 250px; } 7 | #actions { float: right; } 8 | 9 | /* List page only */ 10 | 11 | #stories { 12 | list-style-type: none; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | #stories li { 17 | display: block; 18 | margin: 10px 0; 19 | padding: 1px; 20 | height: 29px; 21 | position: relative; 22 | } 23 | #stories li > a, #stories li > div { 24 | display: block; 25 | overflow: hidden; 26 | white-space: nowrap; 27 | text-decoration: none; 28 | } 29 | #stories li > a, #stories li > div, #stories li .emblems { 30 | height: 27px; 31 | } 32 | #stories li .emblems { 33 | display: block; 34 | position: absolute; 35 | top: 0; 36 | right: 0; 37 | padding: 0 0 0 5px; 38 | background-color: #ffffff; 39 | } 40 | #stories li .emblems span.label { 41 | font-weight: normal; 42 | } 43 | 44 | #stories li, #stories li > a { 45 | background-color: #ffffff; 46 | color: #000000; 47 | } 48 | #stories li:hover, #stories li:hover > a { 49 | background-color: #000000; 50 | color: #ffffff; 51 | } 52 | #stories li:hover .emblems { 53 | background-color: #000000; 54 | } 55 | 56 | #story-progress { 57 | display: block; 58 | position: absolute; 59 | bottom: 6px; 60 | left: 50%; 61 | width: 1000px; 62 | margin-left: -500px; 63 | z-index: 1000; 64 | opacity: 0; 65 | } 66 | 67 | #story-progress span, #story-progress input { 68 | display: block; 69 | float: left; 70 | height: 26px; 71 | } 72 | 73 | #story-progress span { 74 | width: 190px; 75 | } 76 | 77 | #story-progress #story-page { 78 | text-align: right; 79 | } 80 | 81 | #story-progress input { 82 | width: 600px; 83 | margin-left: 10px; 84 | margin-right: 10px; 85 | } 86 | 87 | #story-progress:hover { 88 | opacity: 1.0; 89 | } 90 | -------------------------------------------------------------------------------- /lib/storys/story.rb: -------------------------------------------------------------------------------- 1 | class Storys::Story 2 | attr_reader :package 3 | attr_reader :path 4 | 5 | def initialize(package, path) 6 | @package = package 7 | @path = path 8 | end 9 | 10 | #It is assumed that the HTML document is a valid HTML expression of an NSF document 11 | def html 12 | @html ||= File.read(@path) 13 | end 14 | 15 | def path_hash 16 | Digest::SHA256.hexdigest(path.to_s)[0..16] 17 | end 18 | 19 | def url 20 | package.pathname_to_url(path, package.app_path) 21 | end 22 | 23 | def title 24 | title = title_from_html 25 | title = path.basename.to_s.chomp(path.extname.to_s) if title == "" 26 | 27 | directory_path = path.relative_path_from(package.path).dirname.to_s 28 | 29 | title = "#{directory_path}/#{title}" unless directory_path == "" || directory_path == "." 30 | title = title.gsub("/", " / ") 31 | 32 | title 33 | end 34 | 35 | def self.from_hash(package, data) 36 | Storys::Story.new(package, package.url_to_pathname(Addressable::URI.parse(data["url"]))) 37 | end 38 | 39 | def to_hash 40 | { 41 | "url" => url, 42 | "wordCount" => word_count_from_html, 43 | "title" => title, 44 | "publishedOn" => path.mtime.to_i, 45 | "key" => path_hash 46 | } 47 | end 48 | 49 | def update_manifest 50 | manifest_path = package.pathname_to_url(package.app_path + "manifest", path.dirname) 51 | new_html = html.sub(//, "") 52 | path.write(new_html, preserve_mtime: true) 53 | end 54 | 55 | def title_from_html 56 | html =~ /(.*?)<\/title>/m 57 | $1 ? CGI::unescapeHTML($1.gsub(/\s+/, " ").strip) : "" 58 | end 59 | 60 | def word_count_from_html 61 | html =~ /(.*?)<\/body>/m 62 | body = CGI::unescapeHTML($1.gsub(/<\/?(p|b|i|h[1234567]).*?>/m, " ")) 63 | (title + " " + (body ? body : "")).split(/\s+/).length 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/storys/update.rb: -------------------------------------------------------------------------------- 1 | class Storys::Update 2 | attr_reader :package 3 | attr_accessor :stories 4 | 5 | def initialize(package) 6 | @package = package 7 | 8 | @files = package.path.descendant_files.reject { |p| p.basename.to_s[0..0] == '.' } 9 | @stories = [] 10 | #load_data 11 | convert_files 12 | process 13 | save_data 14 | puts "\nDone!" 15 | end 16 | 17 | def load_data 18 | self.stories = (Storys::Package.load_json(package.app_path + "data.json") || []).map { |b| Storys::Story.from_hash(package, b) } 19 | end 20 | 21 | def save_data 22 | puts "\nWriting out JSON file" 23 | stories_hashes = [] 24 | stories.each_with_index do |s, i| 25 | $stdout.write "\rProcessing #{i + 1} of #{stories.length} (#{(((i + 1) / stories.length.to_f) * 100.0).round}%)" 26 | $stdout.flush 27 | 28 | stories_hashes << s.to_hash 29 | end 30 | Storys::Package.save_json(package.app_path + "data.json", stories_hashes) 31 | end 32 | 33 | def process 34 | puts "\nLoading files" 35 | each_file do |f| 36 | created f 37 | end 38 | #handle deleted first 39 | #@files.each do |f| 40 | # puts "f: #{f.inspect}" 41 | # story = stories.find { |b| b.path == f } 42 | # if story 43 | # updated(story) 44 | # else 45 | # created(f) 46 | # end 47 | #end 48 | end 49 | 50 | def deleted 51 | # 52 | end 53 | 54 | def created(path) 55 | story = Storys::Story.new(package, path) 56 | story.update_manifest 57 | stories << story 58 | end 59 | 60 | def updated(story) 61 | puts "updating: #{story.inspect}" 62 | # 63 | end 64 | 65 | def convert_files 66 | puts "\nConverting files to NSF format..." 67 | each_file do |f| 68 | convert_file f 69 | end 70 | end 71 | 72 | def convert_file(path) 73 | doc = Nsf::Document.from_html(path.read) 74 | new_path = path.update_ext(".html") 75 | new_path.write(doc.to_html, preserve_mtime: true) 76 | end 77 | 78 | def each_file 79 | @files.each_with_index do |f, i| 80 | $stdout.write "\rProcessing #{i + 1} of #{@files.length} (#{(((i + 1) / @files.length.to_f) * 100.0).round}%)" 81 | $stdout.flush 82 | 83 | yield f 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/storys/package.rb: -------------------------------------------------------------------------------- 1 | class Storys::Package 2 | attr_reader :path 3 | attr_reader :app_path 4 | 5 | def initialize(path) 6 | raise "path must be an instance of Pathname" unless path.is_a?(Pathname) 7 | 8 | @path = path 9 | @app_path = path + ".storys/" 10 | end 11 | 12 | def pathname_to_url(path, relative_from) 13 | URI.escape(path.relative_path_from(relative_from).cleanpath.to_s) 14 | end 15 | 16 | #FIXME: Doesn't work! 17 | def url_to_pathname(url) 18 | path = Addressable::URI.unencode_component(url.normalized_path) 19 | path.gsub!(/^\//, "") #Make relative, if we allow mounting at a different root URL this will need to remove the root instead of just '/' 20 | root_url_path + path 21 | end 22 | 23 | def update 24 | app_path.mkdir unless File.exists?(app_path) 25 | Storys::Update.new(self) 26 | update_app 27 | end 28 | 29 | def update_app 30 | dev = ENV["STORYS_ENV"] == "development" 31 | 32 | (app_children_paths + [manifest_path]).each do |file| 33 | storys_file = app_path + file.basename 34 | FileUtils.rm_rf(storys_file, :verbose => dev) 35 | end 36 | 37 | if dev 38 | app_children_paths.each do |file| 39 | storys_file = app_path + file.basename 40 | FileUtils.ln_sf(file, storys_file, :verbose => dev) 41 | end 42 | else 43 | FileUtils.cp_r(Storys::Package.gem_path + "app/.", app_path, :verbose => dev) 44 | save_manifest 45 | end 46 | 47 | FileUtils.chmod_R(0755, app_path, :verbose => dev) 48 | end 49 | 50 | def save_manifest 51 | File.open(manifest_path, "w") do |f| 52 | f << <<-EOFSM 53 | CACHE MANIFEST 54 | # Timestamp #{Time.now.to_i} 55 | CACHE: 56 | css/lib/bootstrap.css 57 | css/app.css 58 | css/views.index.css 59 | css/views.show.css 60 | js/lib/jquery-2.0.3.js 61 | js/lib/bootstrap.js 62 | js/lib/underscore.js 63 | js/lib/handlebars-v1.3.0.js 64 | js/framework.js 65 | js/controllers.index.js 66 | js/controllers.show.js 67 | js/app.js 68 | fonts/lib/glyphicons-halflings-regular.eot 69 | fonts/lib/glyphicons-halflings-regular.svg 70 | fonts/lib/glyphicons-halflings-regular.ttf 71 | fonts/lib/glyphicons-halflings-regular.woff 72 | data.json 73 | EOFSM 74 | end 75 | end 76 | 77 | def manifest_path 78 | app_path + "manifest" 79 | end 80 | 81 | 82 | def self.gem_path 83 | Pathname.new(__FILE__).dirname.parent.parent 84 | end 85 | 86 | def self.load_json(path) 87 | if File.exists?(path) 88 | JSON.parse(File.read(path)) 89 | else 90 | nil 91 | end 92 | end 93 | 94 | def self.save_json(path, data) 95 | File.open(path, "w") { |f| f << data.to_json } 96 | end 97 | 98 | private 99 | def app_children_paths 100 | gem_app_path = Storys::Package.gem_path + "app/" 101 | gem_app_path.children.reject { |f| f.basename.to_s == "img"} #TODO: Deal with this directory properly 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /app/js/framework.js: -------------------------------------------------------------------------------- 1 | var utils = {}; 2 | 3 | utils.location = function(changes) { 4 | if(arguments.length == 1) { 5 | var output = _.extend(utils.location(), changes); 6 | location.hash = "#" + [output.controller].concat(output.params).join("/") + "!" + output.hash; 7 | } 8 | else { 9 | var route_hash = location.hash.substr(1).split("!"); 10 | var route = route_hash[0]; 11 | var controller = route.split("/")[0]; 12 | var params = route.split("/").slice(1); 13 | var hash = route_hash.slice(1).join("!"); 14 | return { 15 | route: route, //Don't set this 16 | controller: controller, 17 | params: params, 18 | hash: hash 19 | }; 20 | } 21 | } 22 | 23 | utils.page = function(index, max) { 24 | if(arguments.length == 2) { 25 | if(isNaN(index) || index < 1) index = 1; 26 | if(index > max) index = max; 27 | utils.location({ hash: index }); 28 | } 29 | else if(arguments.length == 1) { 30 | utils.location({ hash: index }); 31 | } 32 | else { 33 | var index = parseInt(utils.location().hash); 34 | if(isNaN(index) || index < 1) index = 1; 35 | return index; 36 | } 37 | } 38 | 39 | utils.scrollDistanceFromBottom = function() { 40 | return utils.pageHeight() - (window.pageYOffset + self.innerHeight); 41 | } 42 | 43 | utils.pageHeight = function() { 44 | return $(".current-view").height(); 45 | } 46 | 47 | utils.nearBottomOfPage = function() { 48 | return utils.scrollDistanceFromBottom() < 250; 49 | } 50 | 51 | utils.pages = function(array, perPage) { 52 | return Math.ceil(array.length / (perPage + 0.0)); 53 | } 54 | 55 | utils.paginate = function(array, perPage) { 56 | var page = utils.page(); 57 | return array.slice((page - 1) * perPage, page * perPage); 58 | } 59 | 60 | var router = function() { 61 | var _this = this; 62 | 63 | var currentController = null; 64 | var currentRoute = null; 65 | 66 | this.init = function() { 67 | _.each(controllers, function(controller) { 68 | controller.setup(); 69 | }); 70 | 71 | $(window).bind("hashchange", function() { 72 | var route = utils.location().route; 73 | if(route == "") return; 74 | 75 | if(route != currentRoute) { 76 | console.log("changing route from ", currentRoute, " to ", route); 77 | currentRoute = route; 78 | 79 | if(currentController) { 80 | $(".current-view .navbar-content").append($("#navbar-content-target").contents().detach()); 81 | currentController.destroy(); 82 | delete currentController; 83 | } 84 | 85 | var controllerFunction = controllers[utils.location().controller]; 86 | currentController = new (controllerFunction.bind.apply(controllerFunction, [null].concat(utils.location().params))); 87 | currentController.init(); 88 | $("#navbar-content-target").empty().append($(".current-view .navbar-content").contents().detach()); 89 | } 90 | 91 | currentController.render(); 92 | }).trigger("hashchange"); 93 | } 94 | }; 95 | 96 | var controllers = {}; 97 | -------------------------------------------------------------------------------- /app/js/controllers.index.js: -------------------------------------------------------------------------------- 1 | var lastControllerLocation = "#index!1"; 2 | 3 | controllers.index = function(search, sort, sortDirection) { 4 | var _this = this; 5 | 6 | function sortFor(type) { 7 | if(!type) type = "publishedOn"; 8 | 9 | if(type == "publishedOn") return function(story) { 10 | return story.publishedOn; 11 | }; 12 | if(type == "wordCount") return function(story) { 13 | return story.wordCount; 14 | }; 15 | if(type == "title") return function(story) { 16 | return story.title.toLowerCase(); 17 | }; 18 | } 19 | 20 | var storys = store; 21 | if(search && search != "") { 22 | var words = search.split(/\s+/); 23 | _.each(words, function(word) { 24 | regex = RegExp(word, "i"); 25 | storys = _.filter(storys, function(story) { 26 | return story.title.match(regex); 27 | }); 28 | }); 29 | } 30 | if(!sort) sort = "publishedOn"; 31 | storys = _.sortBy(storys, sortFor(sort)); 32 | 33 | if(!sortDirection) sortDirection = "desc"; 34 | if(sortDirection == "desc") storys = storys.reverse(); 35 | 36 | function getVisits(key) { 37 | return parseInt(localStorage["visited." + key]); 38 | } 39 | 40 | function addStories(storys) { 41 | if($("#stories > li").length) return; 42 | 43 | _.each(storys, function(story) { 44 | story.visits = getVisits(story.key); 45 | story.pages = Math.ceil(story.wordCount / 300); 46 | $("#stories").append(controllers.index.storyTemplate({ story: story })); 47 | }); 48 | } 49 | 50 | this.init = function() { 51 | console.log("starting index"); 52 | 53 | $("#search").bind("keydown", function(event) { 54 | event.stopPropagation(); 55 | if(event.keyCode == 13) { 56 | event.preventDefault(); 57 | utils.location({ params: [$("#search").val(), sort, sortDirection], hash: "1" }); 58 | } 59 | }); 60 | 61 | $("#clear-search").bind("click", function() { 62 | $("#search").val(""); 63 | event.preventDefault(); 64 | location.href = "#index!1"; 65 | }); 66 | 67 | $(".sort button").bind("click", function(event) { 68 | event.preventDefault(); 69 | utils.location({ params: [search, $(this).data("sort"), sortDirection], hash: "1" }); 70 | }); 71 | 72 | $(".sort button").removeClass("active"); 73 | $(".sort button[data-sort=" + sort + "]").addClass("active"); 74 | 75 | $(".sort-direction button").bind("click", function(event) { 76 | event.preventDefault(); 77 | console.log("clicked sorter", $(this).data("sort-direction")); 78 | utils.location({ params: [search, sort, $(this).data("sort-direction")], hash: "1" }); 79 | }); 80 | 81 | $(".sort-direction button").removeClass("active"); 82 | $(".sort-direction button[data-sort-direction=" + sortDirection + "]").addClass("active"); 83 | 84 | $("#view-index").show().addClass("current-view"); 85 | addStories(storys); 86 | } 87 | 88 | this.render = function() { 89 | lastControllerLocation = location.hash; 90 | } 91 | 92 | this.destroy = function() { 93 | console.log("destroying index"); 94 | $("#search").unbind("keydown"); 95 | $("#clear-search").unbind("click"); 96 | $(".sort button").unbind("click"); 97 | $(".sort-direction button").unbind("click"); 98 | $("#stories").empty(); 99 | $("#view-index").hide().removeClass("current-view"); 100 | } 101 | } 102 | 103 | controllers.index.setup = function() { 104 | controllers.index.storyTemplate = Handlebars.compile($("#stories-template").remove().html()); 105 | } 106 | -------------------------------------------------------------------------------- /spec/story_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Storys::Story do 4 | def html_factory(title = "Test", body = "This is an example sentence with eight words.") 5 | <<-EOF 6 | 7 | 8 | 9 | 10 | #{"#{title}" unless title.nil?} 11 | 12 | #{"\n #{body}\n " unless body.nil?} 13 | 14 | EOF 15 | end 16 | 17 | context "#title" do 18 | context "basic html" do 19 | before { Storys::Story.any_instance.stub(:html).and_return { html_factory } } 20 | context "file in root directory" do 21 | let!(:story) { build :story } 22 | it { expect(story.title).to eq("Test") } 23 | end 24 | context "file in sub directory" do 25 | let!(:story) { build :story, path: Pathname.new("a/b.html") } 26 | it { expect(story.title).to eq("a / Test") } 27 | end 28 | context "file in a deeply nested directory" do 29 | let!(:story) { build :story, path: Pathname.new("alpha/beta/gamma/delta/epsilon.html") } 30 | it { expect(story.title).to eq("alpha / beta / gamma / delta / Test") } 31 | end 32 | end 33 | 34 | context "no title in html" do 35 | before { Storys::Story.any_instance.stub(:html).and_return { html_factory("") } } 36 | context "file in root directory" do 37 | let!(:story) { build :story, path: Pathname.new("zetta.html") } 38 | it { expect(story.title).to eq("zetta") } 39 | end 40 | context "file in sub directory" do 41 | let!(:story) { build :story, path: Pathname.new("a/b.html") } 42 | it { expect(story.title).to eq("a / b") } 43 | end 44 | context "file in a deeply nested directory" do 45 | let!(:story) { build :story, path: Pathname.new("alpha/beta/gamma/delta/epsilon.html") } 46 | it { expect(story.title).to eq("alpha / beta / gamma / delta / epsilon") } 47 | end 48 | end 49 | end 50 | 51 | context "#title_from_html" do 52 | let!(:story) { build :story } 53 | context "is present" do 54 | before { story.stub(:html).and_return { html_factory } } 55 | it { expect(story.title_from_html).to eq("Test") } 56 | end 57 | context "is present and complex" do 58 | before { story.stub(:html).and_return { html_factory("A Complex %^%*&^*% tITLE\nWith embedded newline") } } 59 | it { expect(story.title_from_html).to eq("A Complex %^%*&^*% tITLE With embedded newline") } 60 | end 61 | context "is present and has extra spaces" do 62 | before { story.stub(:html).and_return { html_factory("SPAAAAAACE! And some more ") } } 63 | it { expect(story.title_from_html).to eq("SPAAAAAACE! And some more") } 64 | end 65 | context "is empty" do 66 | before { story.stub(:html).and_return { html_factory("") } } 67 | it { expect(story.title_from_html).to eq("") } 68 | end 69 | context "is missing" do 70 | before { story.stub(:html).and_return { html_factory(nil) } } 71 | it { expect(story.title_from_html).to eq("") } 72 | end 73 | end 74 | 75 | context "#word_count_from_html" do 76 | let!(:story) { build :story } 77 | context "has some words" do 78 | before { story.stub(:html).and_return { html_factory } } 79 | it { expect(story.word_count_from_html).to eq(9) } 80 | end 81 | context "has words but no title" do 82 | before { story.stub(:html).and_return { html_factory(nil) } } 83 | it { expect(story.word_count_from_html).to eq(9) } 84 | end 85 | context "is empty" do 86 | before { story.stub(:html).and_return { html_factory("Test", "") } } 87 | it { expect(story.word_count_from_html).to eq(1) } 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Storys 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Published On 55 | Pages 56 | Title 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Clear 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {{story.title}} 77 | 78 | {{#if story.visits}} 79 | 80 | 81 | {{story.visits}} 82 | 83 | {{/if}} 84 | 85 | 86 | {{story.pages}} 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /app/js/lib/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.5.2 2 | // http://underscorejs.org 3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `exports` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Establish the object that gets returned to break out of a loop iteration. 18 | var breaker = {}; 19 | 20 | // Save bytes in the minified (but not gzipped) version: 21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 22 | 23 | // Create quick reference variables for speed access to core prototypes. 24 | var 25 | push = ArrayProto.push, 26 | slice = ArrayProto.slice, 27 | concat = ArrayProto.concat, 28 | toString = ObjProto.toString, 29 | hasOwnProperty = ObjProto.hasOwnProperty; 30 | 31 | // All **ECMAScript 5** native function implementations that we hope to use 32 | // are declared here. 33 | var 34 | nativeForEach = ArrayProto.forEach, 35 | nativeMap = ArrayProto.map, 36 | nativeReduce = ArrayProto.reduce, 37 | nativeReduceRight = ArrayProto.reduceRight, 38 | nativeFilter = ArrayProto.filter, 39 | nativeEvery = ArrayProto.every, 40 | nativeSome = ArrayProto.some, 41 | nativeIndexOf = ArrayProto.indexOf, 42 | nativeLastIndexOf = ArrayProto.lastIndexOf, 43 | nativeIsArray = Array.isArray, 44 | nativeKeys = Object.keys, 45 | nativeBind = FuncProto.bind; 46 | 47 | // Create a safe reference to the Underscore object for use below. 48 | var _ = function(obj) { 49 | if (obj instanceof _) return obj; 50 | if (!(this instanceof _)) return new _(obj); 51 | this._wrapped = obj; 52 | }; 53 | 54 | // Export the Underscore object for **Node.js**, with 55 | // backwards-compatibility for the old `require()` API. If we're in 56 | // the browser, add `_` as a global object via a string identifier, 57 | // for Closure Compiler "advanced" mode. 58 | if (typeof exports !== 'undefined') { 59 | if (typeof module !== 'undefined' && module.exports) { 60 | exports = module.exports = _; 61 | } 62 | exports._ = _; 63 | } else { 64 | root._ = _; 65 | } 66 | 67 | // Current version. 68 | _.VERSION = '1.5.2'; 69 | 70 | // Collection Functions 71 | // -------------------- 72 | 73 | // The cornerstone, an `each` implementation, aka `forEach`. 74 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 75 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 76 | var each = _.each = _.forEach = function(obj, iterator, context) { 77 | if (obj == null) return; 78 | if (nativeForEach && obj.forEach === nativeForEach) { 79 | obj.forEach(iterator, context); 80 | } else if (obj.length === +obj.length) { 81 | for (var i = 0, length = obj.length; i < length; i++) { 82 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 83 | } 84 | } else { 85 | var keys = _.keys(obj); 86 | for (var i = 0, length = keys.length; i < length; i++) { 87 | if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; 88 | } 89 | } 90 | }; 91 | 92 | // Return the results of applying the iterator to each element. 93 | // Delegates to **ECMAScript 5**'s native `map` if available. 94 | _.map = _.collect = function(obj, iterator, context) { 95 | var results = []; 96 | if (obj == null) return results; 97 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 98 | each(obj, function(value, index, list) { 99 | results.push(iterator.call(context, value, index, list)); 100 | }); 101 | return results; 102 | }; 103 | 104 | var reduceError = 'Reduce of empty array with no initial value'; 105 | 106 | // **Reduce** builds up a single result from a list of values, aka `inject`, 107 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 108 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 109 | var initial = arguments.length > 2; 110 | if (obj == null) obj = []; 111 | if (nativeReduce && obj.reduce === nativeReduce) { 112 | if (context) iterator = _.bind(iterator, context); 113 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 114 | } 115 | each(obj, function(value, index, list) { 116 | if (!initial) { 117 | memo = value; 118 | initial = true; 119 | } else { 120 | memo = iterator.call(context, memo, value, index, list); 121 | } 122 | }); 123 | if (!initial) throw new TypeError(reduceError); 124 | return memo; 125 | }; 126 | 127 | // The right-associative version of reduce, also known as `foldr`. 128 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 129 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 130 | var initial = arguments.length > 2; 131 | if (obj == null) obj = []; 132 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 133 | if (context) iterator = _.bind(iterator, context); 134 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 135 | } 136 | var length = obj.length; 137 | if (length !== +length) { 138 | var keys = _.keys(obj); 139 | length = keys.length; 140 | } 141 | each(obj, function(value, index, list) { 142 | index = keys ? keys[--length] : --length; 143 | if (!initial) { 144 | memo = obj[index]; 145 | initial = true; 146 | } else { 147 | memo = iterator.call(context, memo, obj[index], index, list); 148 | } 149 | }); 150 | if (!initial) throw new TypeError(reduceError); 151 | return memo; 152 | }; 153 | 154 | // Return the first value which passes a truth test. Aliased as `detect`. 155 | _.find = _.detect = function(obj, iterator, context) { 156 | var result; 157 | any(obj, function(value, index, list) { 158 | if (iterator.call(context, value, index, list)) { 159 | result = value; 160 | return true; 161 | } 162 | }); 163 | return result; 164 | }; 165 | 166 | // Return all the elements that pass a truth test. 167 | // Delegates to **ECMAScript 5**'s native `filter` if available. 168 | // Aliased as `select`. 169 | _.filter = _.select = function(obj, iterator, context) { 170 | var results = []; 171 | if (obj == null) return results; 172 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 173 | each(obj, function(value, index, list) { 174 | if (iterator.call(context, value, index, list)) results.push(value); 175 | }); 176 | return results; 177 | }; 178 | 179 | // Return all the elements for which a truth test fails. 180 | _.reject = function(obj, iterator, context) { 181 | return _.filter(obj, function(value, index, list) { 182 | return !iterator.call(context, value, index, list); 183 | }, context); 184 | }; 185 | 186 | // Determine whether all of the elements match a truth test. 187 | // Delegates to **ECMAScript 5**'s native `every` if available. 188 | // Aliased as `all`. 189 | _.every = _.all = function(obj, iterator, context) { 190 | iterator || (iterator = _.identity); 191 | var result = true; 192 | if (obj == null) return result; 193 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 194 | each(obj, function(value, index, list) { 195 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 196 | }); 197 | return !!result; 198 | }; 199 | 200 | // Determine if at least one element in the object matches a truth test. 201 | // Delegates to **ECMAScript 5**'s native `some` if available. 202 | // Aliased as `any`. 203 | var any = _.some = _.any = function(obj, iterator, context) { 204 | iterator || (iterator = _.identity); 205 | var result = false; 206 | if (obj == null) return result; 207 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 208 | each(obj, function(value, index, list) { 209 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 210 | }); 211 | return !!result; 212 | }; 213 | 214 | // Determine if the array or object contains a given value (using `===`). 215 | // Aliased as `include`. 216 | _.contains = _.include = function(obj, target) { 217 | if (obj == null) return false; 218 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 219 | return any(obj, function(value) { 220 | return value === target; 221 | }); 222 | }; 223 | 224 | // Invoke a method (with arguments) on every item in a collection. 225 | _.invoke = function(obj, method) { 226 | var args = slice.call(arguments, 2); 227 | var isFunc = _.isFunction(method); 228 | return _.map(obj, function(value) { 229 | return (isFunc ? method : value[method]).apply(value, args); 230 | }); 231 | }; 232 | 233 | // Convenience version of a common use case of `map`: fetching a property. 234 | _.pluck = function(obj, key) { 235 | return _.map(obj, function(value){ return value[key]; }); 236 | }; 237 | 238 | // Convenience version of a common use case of `filter`: selecting only objects 239 | // containing specific `key:value` pairs. 240 | _.where = function(obj, attrs, first) { 241 | if (_.isEmpty(attrs)) return first ? void 0 : []; 242 | return _[first ? 'find' : 'filter'](obj, function(value) { 243 | for (var key in attrs) { 244 | if (attrs[key] !== value[key]) return false; 245 | } 246 | return true; 247 | }); 248 | }; 249 | 250 | // Convenience version of a common use case of `find`: getting the first object 251 | // containing specific `key:value` pairs. 252 | _.findWhere = function(obj, attrs) { 253 | return _.where(obj, attrs, true); 254 | }; 255 | 256 | // Return the maximum element or (element-based computation). 257 | // Can't optimize arrays of integers longer than 65,535 elements. 258 | // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) 259 | _.max = function(obj, iterator, context) { 260 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 261 | return Math.max.apply(Math, obj); 262 | } 263 | if (!iterator && _.isEmpty(obj)) return -Infinity; 264 | var result = {computed : -Infinity, value: -Infinity}; 265 | each(obj, function(value, index, list) { 266 | var computed = iterator ? iterator.call(context, value, index, list) : value; 267 | computed > result.computed && (result = {value : value, computed : computed}); 268 | }); 269 | return result.value; 270 | }; 271 | 272 | // Return the minimum element (or element-based computation). 273 | _.min = function(obj, iterator, context) { 274 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 275 | return Math.min.apply(Math, obj); 276 | } 277 | if (!iterator && _.isEmpty(obj)) return Infinity; 278 | var result = {computed : Infinity, value: Infinity}; 279 | each(obj, function(value, index, list) { 280 | var computed = iterator ? iterator.call(context, value, index, list) : value; 281 | computed < result.computed && (result = {value : value, computed : computed}); 282 | }); 283 | return result.value; 284 | }; 285 | 286 | // Shuffle an array, using the modern version of the 287 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 288 | _.shuffle = function(obj) { 289 | var rand; 290 | var index = 0; 291 | var shuffled = []; 292 | each(obj, function(value) { 293 | rand = _.random(index++); 294 | shuffled[index - 1] = shuffled[rand]; 295 | shuffled[rand] = value; 296 | }); 297 | return shuffled; 298 | }; 299 | 300 | // Sample **n** random values from an array. 301 | // If **n** is not specified, returns a single random element from the array. 302 | // The internal `guard` argument allows it to work with `map`. 303 | _.sample = function(obj, n, guard) { 304 | if (arguments.length < 2 || guard) { 305 | return obj[_.random(obj.length - 1)]; 306 | } 307 | return _.shuffle(obj).slice(0, Math.max(0, n)); 308 | }; 309 | 310 | // An internal function to generate lookup iterators. 311 | var lookupIterator = function(value) { 312 | return _.isFunction(value) ? value : function(obj){ return obj[value]; }; 313 | }; 314 | 315 | // Sort the object's values by a criterion produced by an iterator. 316 | _.sortBy = function(obj, value, context) { 317 | var iterator = lookupIterator(value); 318 | return _.pluck(_.map(obj, function(value, index, list) { 319 | return { 320 | value: value, 321 | index: index, 322 | criteria: iterator.call(context, value, index, list) 323 | }; 324 | }).sort(function(left, right) { 325 | var a = left.criteria; 326 | var b = right.criteria; 327 | if (a !== b) { 328 | if (a > b || a === void 0) return 1; 329 | if (a < b || b === void 0) return -1; 330 | } 331 | return left.index - right.index; 332 | }), 'value'); 333 | }; 334 | 335 | // An internal function used for aggregate "group by" operations. 336 | var group = function(behavior) { 337 | return function(obj, value, context) { 338 | var result = {}; 339 | var iterator = value == null ? _.identity : lookupIterator(value); 340 | each(obj, function(value, index) { 341 | var key = iterator.call(context, value, index, obj); 342 | behavior(result, key, value); 343 | }); 344 | return result; 345 | }; 346 | }; 347 | 348 | // Groups the object's values by a criterion. Pass either a string attribute 349 | // to group by, or a function that returns the criterion. 350 | _.groupBy = group(function(result, key, value) { 351 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value); 352 | }); 353 | 354 | // Indexes the object's values by a criterion, similar to `groupBy`, but for 355 | // when you know that your index values will be unique. 356 | _.indexBy = group(function(result, key, value) { 357 | result[key] = value; 358 | }); 359 | 360 | // Counts instances of an object that group by a certain criterion. Pass 361 | // either a string attribute to count by, or a function that returns the 362 | // criterion. 363 | _.countBy = group(function(result, key) { 364 | _.has(result, key) ? result[key]++ : result[key] = 1; 365 | }); 366 | 367 | // Use a comparator function to figure out the smallest index at which 368 | // an object should be inserted so as to maintain order. Uses binary search. 369 | _.sortedIndex = function(array, obj, iterator, context) { 370 | iterator = iterator == null ? _.identity : lookupIterator(iterator); 371 | var value = iterator.call(context, obj); 372 | var low = 0, high = array.length; 373 | while (low < high) { 374 | var mid = (low + high) >>> 1; 375 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 376 | } 377 | return low; 378 | }; 379 | 380 | // Safely create a real, live array from anything iterable. 381 | _.toArray = function(obj) { 382 | if (!obj) return []; 383 | if (_.isArray(obj)) return slice.call(obj); 384 | if (obj.length === +obj.length) return _.map(obj, _.identity); 385 | return _.values(obj); 386 | }; 387 | 388 | // Return the number of elements in an object. 389 | _.size = function(obj) { 390 | if (obj == null) return 0; 391 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 392 | }; 393 | 394 | // Array Functions 395 | // --------------- 396 | 397 | // Get the first element of an array. Passing **n** will return the first N 398 | // values in the array. Aliased as `head` and `take`. The **guard** check 399 | // allows it to work with `_.map`. 400 | _.first = _.head = _.take = function(array, n, guard) { 401 | if (array == null) return void 0; 402 | return (n == null) || guard ? array[0] : slice.call(array, 0, n); 403 | }; 404 | 405 | // Returns everything but the last entry of the array. Especially useful on 406 | // the arguments object. Passing **n** will return all the values in 407 | // the array, excluding the last N. The **guard** check allows it to work with 408 | // `_.map`. 409 | _.initial = function(array, n, guard) { 410 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 411 | }; 412 | 413 | // Get the last element of an array. Passing **n** will return the last N 414 | // values in the array. The **guard** check allows it to work with `_.map`. 415 | _.last = function(array, n, guard) { 416 | if (array == null) return void 0; 417 | if ((n == null) || guard) { 418 | return array[array.length - 1]; 419 | } else { 420 | return slice.call(array, Math.max(array.length - n, 0)); 421 | } 422 | }; 423 | 424 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 425 | // Especially useful on the arguments object. Passing an **n** will return 426 | // the rest N values in the array. The **guard** 427 | // check allows it to work with `_.map`. 428 | _.rest = _.tail = _.drop = function(array, n, guard) { 429 | return slice.call(array, (n == null) || guard ? 1 : n); 430 | }; 431 | 432 | // Trim out all falsy values from an array. 433 | _.compact = function(array) { 434 | return _.filter(array, _.identity); 435 | }; 436 | 437 | // Internal implementation of a recursive `flatten` function. 438 | var flatten = function(input, shallow, output) { 439 | if (shallow && _.every(input, _.isArray)) { 440 | return concat.apply(output, input); 441 | } 442 | each(input, function(value) { 443 | if (_.isArray(value) || _.isArguments(value)) { 444 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 445 | } else { 446 | output.push(value); 447 | } 448 | }); 449 | return output; 450 | }; 451 | 452 | // Flatten out an array, either recursively (by default), or just one level. 453 | _.flatten = function(array, shallow) { 454 | return flatten(array, shallow, []); 455 | }; 456 | 457 | // Return a version of the array that does not contain the specified value(s). 458 | _.without = function(array) { 459 | return _.difference(array, slice.call(arguments, 1)); 460 | }; 461 | 462 | // Produce a duplicate-free version of the array. If the array has already 463 | // been sorted, you have the option of using a faster algorithm. 464 | // Aliased as `unique`. 465 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 466 | if (_.isFunction(isSorted)) { 467 | context = iterator; 468 | iterator = isSorted; 469 | isSorted = false; 470 | } 471 | var initial = iterator ? _.map(array, iterator, context) : array; 472 | var results = []; 473 | var seen = []; 474 | each(initial, function(value, index) { 475 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 476 | seen.push(value); 477 | results.push(array[index]); 478 | } 479 | }); 480 | return results; 481 | }; 482 | 483 | // Produce an array that contains the union: each distinct element from all of 484 | // the passed-in arrays. 485 | _.union = function() { 486 | return _.uniq(_.flatten(arguments, true)); 487 | }; 488 | 489 | // Produce an array that contains every item shared between all the 490 | // passed-in arrays. 491 | _.intersection = function(array) { 492 | var rest = slice.call(arguments, 1); 493 | return _.filter(_.uniq(array), function(item) { 494 | return _.every(rest, function(other) { 495 | return _.indexOf(other, item) >= 0; 496 | }); 497 | }); 498 | }; 499 | 500 | // Take the difference between one array and a number of other arrays. 501 | // Only the elements present in just the first array will remain. 502 | _.difference = function(array) { 503 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 504 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 505 | }; 506 | 507 | // Zip together multiple lists into a single array -- elements that share 508 | // an index go together. 509 | _.zip = function() { 510 | var length = _.max(_.pluck(arguments, "length").concat(0)); 511 | var results = new Array(length); 512 | for (var i = 0; i < length; i++) { 513 | results[i] = _.pluck(arguments, '' + i); 514 | } 515 | return results; 516 | }; 517 | 518 | // Converts lists into objects. Pass either a single array of `[key, value]` 519 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 520 | // the corresponding values. 521 | _.object = function(list, values) { 522 | if (list == null) return {}; 523 | var result = {}; 524 | for (var i = 0, length = list.length; i < length; i++) { 525 | if (values) { 526 | result[list[i]] = values[i]; 527 | } else { 528 | result[list[i][0]] = list[i][1]; 529 | } 530 | } 531 | return result; 532 | }; 533 | 534 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 535 | // we need this function. Return the position of the first occurrence of an 536 | // item in an array, or -1 if the item is not included in the array. 537 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 538 | // If the array is large and already in sort order, pass `true` 539 | // for **isSorted** to use binary search. 540 | _.indexOf = function(array, item, isSorted) { 541 | if (array == null) return -1; 542 | var i = 0, length = array.length; 543 | if (isSorted) { 544 | if (typeof isSorted == 'number') { 545 | i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); 546 | } else { 547 | i = _.sortedIndex(array, item); 548 | return array[i] === item ? i : -1; 549 | } 550 | } 551 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 552 | for (; i < length; i++) if (array[i] === item) return i; 553 | return -1; 554 | }; 555 | 556 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 557 | _.lastIndexOf = function(array, item, from) { 558 | if (array == null) return -1; 559 | var hasIndex = from != null; 560 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 561 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 562 | } 563 | var i = (hasIndex ? from : array.length); 564 | while (i--) if (array[i] === item) return i; 565 | return -1; 566 | }; 567 | 568 | // Generate an integer Array containing an arithmetic progression. A port of 569 | // the native Python `range()` function. See 570 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 571 | _.range = function(start, stop, step) { 572 | if (arguments.length <= 1) { 573 | stop = start || 0; 574 | start = 0; 575 | } 576 | step = arguments[2] || 1; 577 | 578 | var length = Math.max(Math.ceil((stop - start) / step), 0); 579 | var idx = 0; 580 | var range = new Array(length); 581 | 582 | while(idx < length) { 583 | range[idx++] = start; 584 | start += step; 585 | } 586 | 587 | return range; 588 | }; 589 | 590 | // Function (ahem) Functions 591 | // ------------------ 592 | 593 | // Reusable constructor function for prototype setting. 594 | var ctor = function(){}; 595 | 596 | // Create a function bound to a given object (assigning `this`, and arguments, 597 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 598 | // available. 599 | _.bind = function(func, context) { 600 | var args, bound; 601 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 602 | if (!_.isFunction(func)) throw new TypeError; 603 | args = slice.call(arguments, 2); 604 | return bound = function() { 605 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 606 | ctor.prototype = func.prototype; 607 | var self = new ctor; 608 | ctor.prototype = null; 609 | var result = func.apply(self, args.concat(slice.call(arguments))); 610 | if (Object(result) === result) return result; 611 | return self; 612 | }; 613 | }; 614 | 615 | // Partially apply a function by creating a version that has had some of its 616 | // arguments pre-filled, without changing its dynamic `this` context. 617 | _.partial = function(func) { 618 | var args = slice.call(arguments, 1); 619 | return function() { 620 | return func.apply(this, args.concat(slice.call(arguments))); 621 | }; 622 | }; 623 | 624 | // Bind all of an object's methods to that object. Useful for ensuring that 625 | // all callbacks defined on an object belong to it. 626 | _.bindAll = function(obj) { 627 | var funcs = slice.call(arguments, 1); 628 | if (funcs.length === 0) throw new Error("bindAll must be passed function names"); 629 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 630 | return obj; 631 | }; 632 | 633 | // Memoize an expensive function by storing its results. 634 | _.memoize = function(func, hasher) { 635 | var memo = {}; 636 | hasher || (hasher = _.identity); 637 | return function() { 638 | var key = hasher.apply(this, arguments); 639 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 640 | }; 641 | }; 642 | 643 | // Delays a function for the given number of milliseconds, and then calls 644 | // it with the arguments supplied. 645 | _.delay = function(func, wait) { 646 | var args = slice.call(arguments, 2); 647 | return setTimeout(function(){ return func.apply(null, args); }, wait); 648 | }; 649 | 650 | // Defers a function, scheduling it to run after the current call stack has 651 | // cleared. 652 | _.defer = function(func) { 653 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 654 | }; 655 | 656 | // Returns a function, that, when invoked, will only be triggered at most once 657 | // during a given window of time. Normally, the throttled function will run 658 | // as much as it can, without ever going more than once per `wait` duration; 659 | // but if you'd like to disable the execution on the leading edge, pass 660 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 661 | _.throttle = function(func, wait, options) { 662 | var context, args, result; 663 | var timeout = null; 664 | var previous = 0; 665 | options || (options = {}); 666 | var later = function() { 667 | previous = options.leading === false ? 0 : new Date; 668 | timeout = null; 669 | result = func.apply(context, args); 670 | }; 671 | return function() { 672 | var now = new Date; 673 | if (!previous && options.leading === false) previous = now; 674 | var remaining = wait - (now - previous); 675 | context = this; 676 | args = arguments; 677 | if (remaining <= 0) { 678 | clearTimeout(timeout); 679 | timeout = null; 680 | previous = now; 681 | result = func.apply(context, args); 682 | } else if (!timeout && options.trailing !== false) { 683 | timeout = setTimeout(later, remaining); 684 | } 685 | return result; 686 | }; 687 | }; 688 | 689 | // Returns a function, that, as long as it continues to be invoked, will not 690 | // be triggered. The function will be called after it stops being called for 691 | // N milliseconds. If `immediate` is passed, trigger the function on the 692 | // leading edge, instead of the trailing. 693 | _.debounce = function(func, wait, immediate) { 694 | var timeout, args, context, timestamp, result; 695 | return function() { 696 | context = this; 697 | args = arguments; 698 | timestamp = new Date(); 699 | var later = function() { 700 | var last = (new Date()) - timestamp; 701 | if (last < wait) { 702 | timeout = setTimeout(later, wait - last); 703 | } else { 704 | timeout = null; 705 | if (!immediate) result = func.apply(context, args); 706 | } 707 | }; 708 | var callNow = immediate && !timeout; 709 | if (!timeout) { 710 | timeout = setTimeout(later, wait); 711 | } 712 | if (callNow) result = func.apply(context, args); 713 | return result; 714 | }; 715 | }; 716 | 717 | // Returns a function that will be executed at most one time, no matter how 718 | // often you call it. Useful for lazy initialization. 719 | _.once = function(func) { 720 | var ran = false, memo; 721 | return function() { 722 | if (ran) return memo; 723 | ran = true; 724 | memo = func.apply(this, arguments); 725 | func = null; 726 | return memo; 727 | }; 728 | }; 729 | 730 | // Returns the first function passed as an argument to the second, 731 | // allowing you to adjust arguments, run code before and after, and 732 | // conditionally execute the original function. 733 | _.wrap = function(func, wrapper) { 734 | return function() { 735 | var args = [func]; 736 | push.apply(args, arguments); 737 | return wrapper.apply(this, args); 738 | }; 739 | }; 740 | 741 | // Returns a function that is the composition of a list of functions, each 742 | // consuming the return value of the function that follows. 743 | _.compose = function() { 744 | var funcs = arguments; 745 | return function() { 746 | var args = arguments; 747 | for (var i = funcs.length - 1; i >= 0; i--) { 748 | args = [funcs[i].apply(this, args)]; 749 | } 750 | return args[0]; 751 | }; 752 | }; 753 | 754 | // Returns a function that will only be executed after being called N times. 755 | _.after = function(times, func) { 756 | return function() { 757 | if (--times < 1) { 758 | return func.apply(this, arguments); 759 | } 760 | }; 761 | }; 762 | 763 | // Object Functions 764 | // ---------------- 765 | 766 | // Retrieve the names of an object's properties. 767 | // Delegates to **ECMAScript 5**'s native `Object.keys` 768 | _.keys = nativeKeys || function(obj) { 769 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 770 | var keys = []; 771 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 772 | return keys; 773 | }; 774 | 775 | // Retrieve the values of an object's properties. 776 | _.values = function(obj) { 777 | var keys = _.keys(obj); 778 | var length = keys.length; 779 | var values = new Array(length); 780 | for (var i = 0; i < length; i++) { 781 | values[i] = obj[keys[i]]; 782 | } 783 | return values; 784 | }; 785 | 786 | // Convert an object into a list of `[key, value]` pairs. 787 | _.pairs = function(obj) { 788 | var keys = _.keys(obj); 789 | var length = keys.length; 790 | var pairs = new Array(length); 791 | for (var i = 0; i < length; i++) { 792 | pairs[i] = [keys[i], obj[keys[i]]]; 793 | } 794 | return pairs; 795 | }; 796 | 797 | // Invert the keys and values of an object. The values must be serializable. 798 | _.invert = function(obj) { 799 | var result = {}; 800 | var keys = _.keys(obj); 801 | for (var i = 0, length = keys.length; i < length; i++) { 802 | result[obj[keys[i]]] = keys[i]; 803 | } 804 | return result; 805 | }; 806 | 807 | // Return a sorted list of the function names available on the object. 808 | // Aliased as `methods` 809 | _.functions = _.methods = function(obj) { 810 | var names = []; 811 | for (var key in obj) { 812 | if (_.isFunction(obj[key])) names.push(key); 813 | } 814 | return names.sort(); 815 | }; 816 | 817 | // Extend a given object with all the properties in passed-in object(s). 818 | _.extend = function(obj) { 819 | each(slice.call(arguments, 1), function(source) { 820 | if (source) { 821 | for (var prop in source) { 822 | obj[prop] = source[prop]; 823 | } 824 | } 825 | }); 826 | return obj; 827 | }; 828 | 829 | // Return a copy of the object only containing the whitelisted properties. 830 | _.pick = function(obj) { 831 | var copy = {}; 832 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 833 | each(keys, function(key) { 834 | if (key in obj) copy[key] = obj[key]; 835 | }); 836 | return copy; 837 | }; 838 | 839 | // Return a copy of the object without the blacklisted properties. 840 | _.omit = function(obj) { 841 | var copy = {}; 842 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 843 | for (var key in obj) { 844 | if (!_.contains(keys, key)) copy[key] = obj[key]; 845 | } 846 | return copy; 847 | }; 848 | 849 | // Fill in a given object with default properties. 850 | _.defaults = function(obj) { 851 | each(slice.call(arguments, 1), function(source) { 852 | if (source) { 853 | for (var prop in source) { 854 | if (obj[prop] === void 0) obj[prop] = source[prop]; 855 | } 856 | } 857 | }); 858 | return obj; 859 | }; 860 | 861 | // Create a (shallow-cloned) duplicate of an object. 862 | _.clone = function(obj) { 863 | if (!_.isObject(obj)) return obj; 864 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 865 | }; 866 | 867 | // Invokes interceptor with the obj, and then returns obj. 868 | // The primary purpose of this method is to "tap into" a method chain, in 869 | // order to perform operations on intermediate results within the chain. 870 | _.tap = function(obj, interceptor) { 871 | interceptor(obj); 872 | return obj; 873 | }; 874 | 875 | // Internal recursive comparison function for `isEqual`. 876 | var eq = function(a, b, aStack, bStack) { 877 | // Identical objects are equal. `0 === -0`, but they aren't identical. 878 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). 879 | if (a === b) return a !== 0 || 1 / a == 1 / b; 880 | // A strict comparison is necessary because `null == undefined`. 881 | if (a == null || b == null) return a === b; 882 | // Unwrap any wrapped objects. 883 | if (a instanceof _) a = a._wrapped; 884 | if (b instanceof _) b = b._wrapped; 885 | // Compare `[[Class]]` names. 886 | var className = toString.call(a); 887 | if (className != toString.call(b)) return false; 888 | switch (className) { 889 | // Strings, numbers, dates, and booleans are compared by value. 890 | case '[object String]': 891 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 892 | // equivalent to `new String("5")`. 893 | return a == String(b); 894 | case '[object Number]': 895 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 896 | // other numeric values. 897 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 898 | case '[object Date]': 899 | case '[object Boolean]': 900 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 901 | // millisecond representations. Note that invalid dates with millisecond representations 902 | // of `NaN` are not equivalent. 903 | return +a == +b; 904 | // RegExps are compared by their source patterns and flags. 905 | case '[object RegExp]': 906 | return a.source == b.source && 907 | a.global == b.global && 908 | a.multiline == b.multiline && 909 | a.ignoreCase == b.ignoreCase; 910 | } 911 | if (typeof a != 'object' || typeof b != 'object') return false; 912 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 913 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 914 | var length = aStack.length; 915 | while (length--) { 916 | // Linear search. Performance is inversely proportional to the number of 917 | // unique nested structures. 918 | if (aStack[length] == a) return bStack[length] == b; 919 | } 920 | // Objects with different constructors are not equivalent, but `Object`s 921 | // from different frames are. 922 | var aCtor = a.constructor, bCtor = b.constructor; 923 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 924 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) { 925 | return false; 926 | } 927 | // Add the first object to the stack of traversed objects. 928 | aStack.push(a); 929 | bStack.push(b); 930 | var size = 0, result = true; 931 | // Recursively compare objects and arrays. 932 | if (className == '[object Array]') { 933 | // Compare array lengths to determine if a deep comparison is necessary. 934 | size = a.length; 935 | result = size == b.length; 936 | if (result) { 937 | // Deep compare the contents, ignoring non-numeric properties. 938 | while (size--) { 939 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 940 | } 941 | } 942 | } else { 943 | // Deep compare objects. 944 | for (var key in a) { 945 | if (_.has(a, key)) { 946 | // Count the expected number of properties. 947 | size++; 948 | // Deep compare each member. 949 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 950 | } 951 | } 952 | // Ensure that both objects contain the same number of properties. 953 | if (result) { 954 | for (key in b) { 955 | if (_.has(b, key) && !(size--)) break; 956 | } 957 | result = !size; 958 | } 959 | } 960 | // Remove the first object from the stack of traversed objects. 961 | aStack.pop(); 962 | bStack.pop(); 963 | return result; 964 | }; 965 | 966 | // Perform a deep comparison to check if two objects are equal. 967 | _.isEqual = function(a, b) { 968 | return eq(a, b, [], []); 969 | }; 970 | 971 | // Is a given array, string, or object empty? 972 | // An "empty" object has no enumerable own-properties. 973 | _.isEmpty = function(obj) { 974 | if (obj == null) return true; 975 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 976 | for (var key in obj) if (_.has(obj, key)) return false; 977 | return true; 978 | }; 979 | 980 | // Is a given value a DOM element? 981 | _.isElement = function(obj) { 982 | return !!(obj && obj.nodeType === 1); 983 | }; 984 | 985 | // Is a given value an array? 986 | // Delegates to ECMA5's native Array.isArray 987 | _.isArray = nativeIsArray || function(obj) { 988 | return toString.call(obj) == '[object Array]'; 989 | }; 990 | 991 | // Is a given variable an object? 992 | _.isObject = function(obj) { 993 | return obj === Object(obj); 994 | }; 995 | 996 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 997 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 998 | _['is' + name] = function(obj) { 999 | return toString.call(obj) == '[object ' + name + ']'; 1000 | }; 1001 | }); 1002 | 1003 | // Define a fallback version of the method in browsers (ahem, IE), where 1004 | // there isn't any inspectable "Arguments" type. 1005 | if (!_.isArguments(arguments)) { 1006 | _.isArguments = function(obj) { 1007 | return !!(obj && _.has(obj, 'callee')); 1008 | }; 1009 | } 1010 | 1011 | // Optimize `isFunction` if appropriate. 1012 | if (typeof (/./) !== 'function') { 1013 | _.isFunction = function(obj) { 1014 | return typeof obj === 'function'; 1015 | }; 1016 | } 1017 | 1018 | // Is a given object a finite number? 1019 | _.isFinite = function(obj) { 1020 | return isFinite(obj) && !isNaN(parseFloat(obj)); 1021 | }; 1022 | 1023 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 1024 | _.isNaN = function(obj) { 1025 | return _.isNumber(obj) && obj != +obj; 1026 | }; 1027 | 1028 | // Is a given value a boolean? 1029 | _.isBoolean = function(obj) { 1030 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 1031 | }; 1032 | 1033 | // Is a given value equal to null? 1034 | _.isNull = function(obj) { 1035 | return obj === null; 1036 | }; 1037 | 1038 | // Is a given variable undefined? 1039 | _.isUndefined = function(obj) { 1040 | return obj === void 0; 1041 | }; 1042 | 1043 | // Shortcut function for checking if an object has a given property directly 1044 | // on itself (in other words, not on a prototype). 1045 | _.has = function(obj, key) { 1046 | return hasOwnProperty.call(obj, key); 1047 | }; 1048 | 1049 | // Utility Functions 1050 | // ----------------- 1051 | 1052 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1053 | // previous owner. Returns a reference to the Underscore object. 1054 | _.noConflict = function() { 1055 | root._ = previousUnderscore; 1056 | return this; 1057 | }; 1058 | 1059 | // Keep the identity function around for default iterators. 1060 | _.identity = function(value) { 1061 | return value; 1062 | }; 1063 | 1064 | // Run a function **n** times. 1065 | _.times = function(n, iterator, context) { 1066 | var accum = Array(Math.max(0, n)); 1067 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); 1068 | return accum; 1069 | }; 1070 | 1071 | // Return a random integer between min and max (inclusive). 1072 | _.random = function(min, max) { 1073 | if (max == null) { 1074 | max = min; 1075 | min = 0; 1076 | } 1077 | return min + Math.floor(Math.random() * (max - min + 1)); 1078 | }; 1079 | 1080 | // List of HTML entities for escaping. 1081 | var entityMap = { 1082 | escape: { 1083 | '&': '&', 1084 | '<': '<', 1085 | '>': '>', 1086 | '"': '"', 1087 | "'": ''' 1088 | } 1089 | }; 1090 | entityMap.unescape = _.invert(entityMap.escape); 1091 | 1092 | // Regexes containing the keys and values listed immediately above. 1093 | var entityRegexes = { 1094 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1095 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1096 | }; 1097 | 1098 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1099 | _.each(['escape', 'unescape'], function(method) { 1100 | _[method] = function(string) { 1101 | if (string == null) return ''; 1102 | return ('' + string).replace(entityRegexes[method], function(match) { 1103 | return entityMap[method][match]; 1104 | }); 1105 | }; 1106 | }); 1107 | 1108 | // If the value of the named `property` is a function then invoke it with the 1109 | // `object` as context; otherwise, return it. 1110 | _.result = function(object, property) { 1111 | if (object == null) return void 0; 1112 | var value = object[property]; 1113 | return _.isFunction(value) ? value.call(object) : value; 1114 | }; 1115 | 1116 | // Add your own custom functions to the Underscore object. 1117 | _.mixin = function(obj) { 1118 | each(_.functions(obj), function(name) { 1119 | var func = _[name] = obj[name]; 1120 | _.prototype[name] = function() { 1121 | var args = [this._wrapped]; 1122 | push.apply(args, arguments); 1123 | return result.call(this, func.apply(_, args)); 1124 | }; 1125 | }); 1126 | }; 1127 | 1128 | // Generate a unique integer id (unique within the entire client session). 1129 | // Useful for temporary DOM ids. 1130 | var idCounter = 0; 1131 | _.uniqueId = function(prefix) { 1132 | var id = ++idCounter + ''; 1133 | return prefix ? prefix + id : id; 1134 | }; 1135 | 1136 | // By default, Underscore uses ERB-style template delimiters, change the 1137 | // following template settings to use alternative delimiters. 1138 | _.templateSettings = { 1139 | evaluate : /<%([\s\S]+?)%>/g, 1140 | interpolate : /<%=([\s\S]+?)%>/g, 1141 | escape : /<%-([\s\S]+?)%>/g 1142 | }; 1143 | 1144 | // When customizing `templateSettings`, if you don't want to define an 1145 | // interpolation, evaluation or escaping regex, we need one that is 1146 | // guaranteed not to match. 1147 | var noMatch = /(.)^/; 1148 | 1149 | // Certain characters need to be escaped so that they can be put into a 1150 | // string literal. 1151 | var escapes = { 1152 | "'": "'", 1153 | '\\': '\\', 1154 | '\r': 'r', 1155 | '\n': 'n', 1156 | '\t': 't', 1157 | '\u2028': 'u2028', 1158 | '\u2029': 'u2029' 1159 | }; 1160 | 1161 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1162 | 1163 | // JavaScript micro-templating, similar to John Resig's implementation. 1164 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1165 | // and correctly escapes quotes within interpolated code. 1166 | _.template = function(text, data, settings) { 1167 | var render; 1168 | settings = _.defaults({}, settings, _.templateSettings); 1169 | 1170 | // Combine delimiters into one regular expression via alternation. 1171 | var matcher = new RegExp([ 1172 | (settings.escape || noMatch).source, 1173 | (settings.interpolate || noMatch).source, 1174 | (settings.evaluate || noMatch).source 1175 | ].join('|') + '|$', 'g'); 1176 | 1177 | // Compile the template source, escaping string literals appropriately. 1178 | var index = 0; 1179 | var source = "__p+='"; 1180 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1181 | source += text.slice(index, offset) 1182 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1183 | 1184 | if (escape) { 1185 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1186 | } 1187 | if (interpolate) { 1188 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1189 | } 1190 | if (evaluate) { 1191 | source += "';\n" + evaluate + "\n__p+='"; 1192 | } 1193 | index = offset + match.length; 1194 | return match; 1195 | }); 1196 | source += "';\n"; 1197 | 1198 | // If a variable is not specified, place data values in local scope. 1199 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1200 | 1201 | source = "var __t,__p='',__j=Array.prototype.join," + 1202 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1203 | source + "return __p;\n"; 1204 | 1205 | try { 1206 | render = new Function(settings.variable || 'obj', '_', source); 1207 | } catch (e) { 1208 | e.source = source; 1209 | throw e; 1210 | } 1211 | 1212 | if (data) return render(data, _); 1213 | var template = function(data) { 1214 | return render.call(this, data, _); 1215 | }; 1216 | 1217 | // Provide the compiled function source as a convenience for precompilation. 1218 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1219 | 1220 | return template; 1221 | }; 1222 | 1223 | // Add a "chain" function, which will delegate to the wrapper. 1224 | _.chain = function(obj) { 1225 | return _(obj).chain(); 1226 | }; 1227 | 1228 | // OOP 1229 | // --------------- 1230 | // If Underscore is called as a function, it returns a wrapped object that 1231 | // can be used OO-style. This wrapper holds altered versions of all the 1232 | // underscore functions. Wrapped objects may be chained. 1233 | 1234 | // Helper function to continue chaining intermediate results. 1235 | var result = function(obj) { 1236 | return this._chain ? _(obj).chain() : obj; 1237 | }; 1238 | 1239 | // Add all of the Underscore functions to the wrapper object. 1240 | _.mixin(_); 1241 | 1242 | // Add all mutator Array functions to the wrapper. 1243 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1244 | var method = ArrayProto[name]; 1245 | _.prototype[name] = function() { 1246 | var obj = this._wrapped; 1247 | method.apply(obj, arguments); 1248 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1249 | return result.call(this, obj); 1250 | }; 1251 | }); 1252 | 1253 | // Add all accessor Array functions to the wrapper. 1254 | each(['concat', 'join', 'slice'], function(name) { 1255 | var method = ArrayProto[name]; 1256 | _.prototype[name] = function() { 1257 | return result.call(this, method.apply(this._wrapped, arguments)); 1258 | }; 1259 | }); 1260 | 1261 | _.extend(_.prototype, { 1262 | 1263 | // Start chaining a wrapped Underscore object. 1264 | chain: function() { 1265 | this._chain = true; 1266 | return this; 1267 | }, 1268 | 1269 | // Extracts the result from a wrapped and chained object. 1270 | value: function() { 1271 | return this._wrapped; 1272 | } 1273 | 1274 | }); 1275 | 1276 | }).call(this); 1277 | -------------------------------------------------------------------------------- /app/fonts/lib/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /app/js/lib/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * Bootstrap: alert.js v3.1.1 3 | * http://getbootstrap.com/javascript/#alerts 4 | * ======================================================================== 5 | * Copyright 2011-2014 Twitter, Inc. 6 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 7 | * ======================================================================== */ 8 | 9 | 10 | +function ($) { 11 | 'use strict'; 12 | 13 | // ALERT CLASS DEFINITION 14 | // ====================== 15 | 16 | var dismiss = '[data-dismiss="alert"]' 17 | var Alert = function (el) { 18 | $(el).on('click', dismiss, this.close) 19 | } 20 | 21 | Alert.prototype.close = function (e) { 22 | var $this = $(this) 23 | var selector = $this.attr('data-target') 24 | 25 | if (!selector) { 26 | selector = $this.attr('href') 27 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 28 | } 29 | 30 | var $parent = $(selector) 31 | 32 | if (e) e.preventDefault() 33 | 34 | if (!$parent.length) { 35 | $parent = $this.hasClass('alert') ? $this : $this.parent() 36 | } 37 | 38 | $parent.trigger(e = $.Event('close.bs.alert')) 39 | 40 | if (e.isDefaultPrevented()) return 41 | 42 | $parent.removeClass('in') 43 | 44 | function removeElement() { 45 | $parent.trigger('closed.bs.alert').remove() 46 | } 47 | 48 | $.support.transition && $parent.hasClass('fade') ? 49 | $parent 50 | .one($.support.transition.end, removeElement) 51 | .emulateTransitionEnd(150) : 52 | removeElement() 53 | } 54 | 55 | 56 | // ALERT PLUGIN DEFINITION 57 | // ======================= 58 | 59 | var old = $.fn.alert 60 | 61 | $.fn.alert = function (option) { 62 | return this.each(function () { 63 | var $this = $(this) 64 | var data = $this.data('bs.alert') 65 | 66 | if (!data) $this.data('bs.alert', (data = new Alert(this))) 67 | if (typeof option == 'string') data[option].call($this) 68 | }) 69 | } 70 | 71 | $.fn.alert.Constructor = Alert 72 | 73 | 74 | // ALERT NO CONFLICT 75 | // ================= 76 | 77 | $.fn.alert.noConflict = function () { 78 | $.fn.alert = old 79 | return this 80 | } 81 | 82 | 83 | // ALERT DATA-API 84 | // ============== 85 | 86 | $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) 87 | 88 | }(jQuery); 89 | 90 | /* ======================================================================== 91 | * Bootstrap: button.js v3.1.1 92 | * http://getbootstrap.com/javascript/#buttons 93 | * ======================================================================== 94 | * Copyright 2011-2014 Twitter, Inc. 95 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 96 | * ======================================================================== */ 97 | 98 | 99 | +function ($) { 100 | 'use strict'; 101 | 102 | // BUTTON PUBLIC CLASS DEFINITION 103 | // ============================== 104 | 105 | var Button = function (element, options) { 106 | this.$element = $(element) 107 | this.options = $.extend({}, Button.DEFAULTS, options) 108 | this.isLoading = false 109 | } 110 | 111 | Button.DEFAULTS = { 112 | loadingText: 'loading...' 113 | } 114 | 115 | Button.prototype.setState = function (state) { 116 | var d = 'disabled' 117 | var $el = this.$element 118 | var val = $el.is('input') ? 'val' : 'html' 119 | var data = $el.data() 120 | 121 | state = state + 'Text' 122 | 123 | if (!data.resetText) $el.data('resetText', $el[val]()) 124 | 125 | $el[val](data[state] || this.options[state]) 126 | 127 | // push to event loop to allow forms to submit 128 | setTimeout($.proxy(function () { 129 | if (state == 'loadingText') { 130 | this.isLoading = true 131 | $el.addClass(d).attr(d, d) 132 | } else if (this.isLoading) { 133 | this.isLoading = false 134 | $el.removeClass(d).removeAttr(d) 135 | } 136 | }, this), 0) 137 | } 138 | 139 | Button.prototype.toggle = function () { 140 | var changed = true 141 | var $parent = this.$element.closest('[data-toggle="buttons"]') 142 | 143 | if ($parent.length) { 144 | var $input = this.$element.find('input') 145 | if ($input.prop('type') == 'radio') { 146 | if ($input.prop('checked') && this.$element.hasClass('active')) changed = false 147 | else $parent.find('.active').removeClass('active') 148 | } 149 | if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') 150 | } 151 | 152 | if (changed) this.$element.toggleClass('active') 153 | } 154 | 155 | 156 | // BUTTON PLUGIN DEFINITION 157 | // ======================== 158 | 159 | var old = $.fn.button 160 | 161 | $.fn.button = function (option) { 162 | return this.each(function () { 163 | var $this = $(this) 164 | var data = $this.data('bs.button') 165 | var options = typeof option == 'object' && option 166 | 167 | if (!data) $this.data('bs.button', (data = new Button(this, options))) 168 | 169 | if (option == 'toggle') data.toggle() 170 | else if (option) data.setState(option) 171 | }) 172 | } 173 | 174 | $.fn.button.Constructor = Button 175 | 176 | 177 | // BUTTON NO CONFLICT 178 | // ================== 179 | 180 | $.fn.button.noConflict = function () { 181 | $.fn.button = old 182 | return this 183 | } 184 | 185 | 186 | // BUTTON DATA-API 187 | // =============== 188 | 189 | $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { 190 | var $btn = $(e.target) 191 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 192 | $btn.button('toggle') 193 | e.preventDefault() 194 | }) 195 | 196 | }(jQuery); 197 | 198 | /* ======================================================================== 199 | * Bootstrap: carousel.js v3.1.1 200 | * http://getbootstrap.com/javascript/#carousel 201 | * ======================================================================== 202 | * Copyright 2011-2014 Twitter, Inc. 203 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 204 | * ======================================================================== */ 205 | 206 | 207 | +function ($) { 208 | 'use strict'; 209 | 210 | // CAROUSEL CLASS DEFINITION 211 | // ========================= 212 | 213 | var Carousel = function (element, options) { 214 | this.$element = $(element) 215 | this.$indicators = this.$element.find('.carousel-indicators') 216 | this.options = options 217 | this.paused = 218 | this.sliding = 219 | this.interval = 220 | this.$active = 221 | this.$items = null 222 | 223 | this.options.pause == 'hover' && this.$element 224 | .on('mouseenter', $.proxy(this.pause, this)) 225 | .on('mouseleave', $.proxy(this.cycle, this)) 226 | } 227 | 228 | Carousel.DEFAULTS = { 229 | interval: 5000, 230 | pause: 'hover', 231 | wrap: true 232 | } 233 | 234 | Carousel.prototype.cycle = function (e) { 235 | e || (this.paused = false) 236 | 237 | this.interval && clearInterval(this.interval) 238 | 239 | this.options.interval 240 | && !this.paused 241 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 242 | 243 | return this 244 | } 245 | 246 | Carousel.prototype.getActiveIndex = function () { 247 | this.$active = this.$element.find('.item.active') 248 | this.$items = this.$active.parent().children() 249 | 250 | return this.$items.index(this.$active) 251 | } 252 | 253 | Carousel.prototype.to = function (pos) { 254 | var that = this 255 | var activeIndex = this.getActiveIndex() 256 | 257 | if (pos > (this.$items.length - 1) || pos < 0) return 258 | 259 | if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) 260 | if (activeIndex == pos) return this.pause().cycle() 261 | 262 | return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) 263 | } 264 | 265 | Carousel.prototype.pause = function (e) { 266 | e || (this.paused = true) 267 | 268 | if (this.$element.find('.next, .prev').length && $.support.transition) { 269 | this.$element.trigger($.support.transition.end) 270 | this.cycle(true) 271 | } 272 | 273 | this.interval = clearInterval(this.interval) 274 | 275 | return this 276 | } 277 | 278 | Carousel.prototype.next = function () { 279 | if (this.sliding) return 280 | return this.slide('next') 281 | } 282 | 283 | Carousel.prototype.prev = function () { 284 | if (this.sliding) return 285 | return this.slide('prev') 286 | } 287 | 288 | Carousel.prototype.slide = function (type, next) { 289 | var $active = this.$element.find('.item.active') 290 | var $next = next || $active[type]() 291 | var isCycling = this.interval 292 | var direction = type == 'next' ? 'left' : 'right' 293 | var fallback = type == 'next' ? 'first' : 'last' 294 | var that = this 295 | 296 | if (!$next.length) { 297 | if (!this.options.wrap) return 298 | $next = this.$element.find('.item')[fallback]() 299 | } 300 | 301 | if ($next.hasClass('active')) return this.sliding = false 302 | 303 | var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) 304 | this.$element.trigger(e) 305 | if (e.isDefaultPrevented()) return 306 | 307 | this.sliding = true 308 | 309 | isCycling && this.pause() 310 | 311 | if (this.$indicators.length) { 312 | this.$indicators.find('.active').removeClass('active') 313 | this.$element.one('slid.bs.carousel', function () { 314 | var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) 315 | $nextIndicator && $nextIndicator.addClass('active') 316 | }) 317 | } 318 | 319 | if ($.support.transition && this.$element.hasClass('slide')) { 320 | $next.addClass(type) 321 | $next[0].offsetWidth // force reflow 322 | $active.addClass(direction) 323 | $next.addClass(direction) 324 | $active 325 | .one($.support.transition.end, function () { 326 | $next.removeClass([type, direction].join(' ')).addClass('active') 327 | $active.removeClass(['active', direction].join(' ')) 328 | that.sliding = false 329 | setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) 330 | }) 331 | .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000) 332 | } else { 333 | $active.removeClass('active') 334 | $next.addClass('active') 335 | this.sliding = false 336 | this.$element.trigger('slid.bs.carousel') 337 | } 338 | 339 | isCycling && this.cycle() 340 | 341 | return this 342 | } 343 | 344 | 345 | // CAROUSEL PLUGIN DEFINITION 346 | // ========================== 347 | 348 | var old = $.fn.carousel 349 | 350 | $.fn.carousel = function (option) { 351 | return this.each(function () { 352 | var $this = $(this) 353 | var data = $this.data('bs.carousel') 354 | var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) 355 | var action = typeof option == 'string' ? option : options.slide 356 | 357 | if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) 358 | if (typeof option == 'number') data.to(option) 359 | else if (action) data[action]() 360 | else if (options.interval) data.pause().cycle() 361 | }) 362 | } 363 | 364 | $.fn.carousel.Constructor = Carousel 365 | 366 | 367 | // CAROUSEL NO CONFLICT 368 | // ==================== 369 | 370 | $.fn.carousel.noConflict = function () { 371 | $.fn.carousel = old 372 | return this 373 | } 374 | 375 | 376 | // CAROUSEL DATA-API 377 | // ================= 378 | 379 | $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { 380 | var $this = $(this), href 381 | var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 382 | var options = $.extend({}, $target.data(), $this.data()) 383 | var slideIndex = $this.attr('data-slide-to') 384 | if (slideIndex) options.interval = false 385 | 386 | $target.carousel(options) 387 | 388 | if (slideIndex = $this.attr('data-slide-to')) { 389 | $target.data('bs.carousel').to(slideIndex) 390 | } 391 | 392 | e.preventDefault() 393 | }) 394 | 395 | $(window).on('load', function () { 396 | $('[data-ride="carousel"]').each(function () { 397 | var $carousel = $(this) 398 | $carousel.carousel($carousel.data()) 399 | }) 400 | }) 401 | 402 | }(jQuery); 403 | 404 | /* ======================================================================== 405 | * Bootstrap: dropdown.js v3.1.1 406 | * http://getbootstrap.com/javascript/#dropdowns 407 | * ======================================================================== 408 | * Copyright 2011-2014 Twitter, Inc. 409 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 410 | * ======================================================================== */ 411 | 412 | 413 | +function ($) { 414 | 'use strict'; 415 | 416 | // DROPDOWN CLASS DEFINITION 417 | // ========================= 418 | 419 | var backdrop = '.dropdown-backdrop' 420 | var toggle = '[data-toggle=dropdown]' 421 | var Dropdown = function (element) { 422 | $(element).on('click.bs.dropdown', this.toggle) 423 | } 424 | 425 | Dropdown.prototype.toggle = function (e) { 426 | var $this = $(this) 427 | 428 | if ($this.is('.disabled, :disabled')) return 429 | 430 | var $parent = getParent($this) 431 | var isActive = $parent.hasClass('open') 432 | 433 | clearMenus() 434 | 435 | if (!isActive) { 436 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { 437 | // if mobile we use a backdrop because click events don't delegate 438 | $('').insertAfter($(this)).on('click', clearMenus) 439 | } 440 | 441 | var relatedTarget = { relatedTarget: this } 442 | $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) 443 | 444 | if (e.isDefaultPrevented()) return 445 | 446 | $parent 447 | .toggleClass('open') 448 | .trigger('shown.bs.dropdown', relatedTarget) 449 | 450 | $this.focus() 451 | } 452 | 453 | return false 454 | } 455 | 456 | Dropdown.prototype.keydown = function (e) { 457 | if (!/(38|40|27)/.test(e.keyCode)) return 458 | 459 | var $this = $(this) 460 | 461 | e.preventDefault() 462 | e.stopPropagation() 463 | 464 | if ($this.is('.disabled, :disabled')) return 465 | 466 | var $parent = getParent($this) 467 | var isActive = $parent.hasClass('open') 468 | 469 | if (!isActive || (isActive && e.keyCode == 27)) { 470 | if (e.which == 27) $parent.find(toggle).focus() 471 | return $this.click() 472 | } 473 | 474 | var desc = ' li:not(.divider):visible a' 475 | var $items = $parent.find('[role=menu]' + desc + ', [role=listbox]' + desc) 476 | 477 | if (!$items.length) return 478 | 479 | var index = $items.index($items.filter(':focus')) 480 | 481 | if (e.keyCode == 38 && index > 0) index-- // up 482 | if (e.keyCode == 40 && index < $items.length - 1) index++ // down 483 | if (!~index) index = 0 484 | 485 | $items.eq(index).focus() 486 | } 487 | 488 | function clearMenus(e) { 489 | $(backdrop).remove() 490 | $(toggle).each(function () { 491 | var $parent = getParent($(this)) 492 | var relatedTarget = { relatedTarget: this } 493 | if (!$parent.hasClass('open')) return 494 | $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) 495 | if (e.isDefaultPrevented()) return 496 | $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) 497 | }) 498 | } 499 | 500 | function getParent($this) { 501 | var selector = $this.attr('data-target') 502 | 503 | if (!selector) { 504 | selector = $this.attr('href') 505 | selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 506 | } 507 | 508 | var $parent = selector && $(selector) 509 | 510 | return $parent && $parent.length ? $parent : $this.parent() 511 | } 512 | 513 | 514 | // DROPDOWN PLUGIN DEFINITION 515 | // ========================== 516 | 517 | var old = $.fn.dropdown 518 | 519 | $.fn.dropdown = function (option) { 520 | return this.each(function () { 521 | var $this = $(this) 522 | var data = $this.data('bs.dropdown') 523 | 524 | if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) 525 | if (typeof option == 'string') data[option].call($this) 526 | }) 527 | } 528 | 529 | $.fn.dropdown.Constructor = Dropdown 530 | 531 | 532 | // DROPDOWN NO CONFLICT 533 | // ==================== 534 | 535 | $.fn.dropdown.noConflict = function () { 536 | $.fn.dropdown = old 537 | return this 538 | } 539 | 540 | 541 | // APPLY TO STANDARD DROPDOWN ELEMENTS 542 | // =================================== 543 | 544 | $(document) 545 | .on('click.bs.dropdown.data-api', clearMenus) 546 | .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) 547 | .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) 548 | .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu], [role=listbox]', Dropdown.prototype.keydown) 549 | 550 | }(jQuery); 551 | 552 | /* ======================================================================== 553 | * Bootstrap: modal.js v3.1.1 554 | * http://getbootstrap.com/javascript/#modals 555 | * ======================================================================== 556 | * Copyright 2011-2014 Twitter, Inc. 557 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 558 | * ======================================================================== */ 559 | 560 | 561 | +function ($) { 562 | 'use strict'; 563 | 564 | // MODAL CLASS DEFINITION 565 | // ====================== 566 | 567 | var Modal = function (element, options) { 568 | this.options = options 569 | this.$element = $(element) 570 | this.$backdrop = 571 | this.isShown = null 572 | 573 | if (this.options.remote) { 574 | this.$element 575 | .find('.modal-content') 576 | .load(this.options.remote, $.proxy(function () { 577 | this.$element.trigger('loaded.bs.modal') 578 | }, this)) 579 | } 580 | } 581 | 582 | Modal.DEFAULTS = { 583 | backdrop: true, 584 | keyboard: true, 585 | show: true 586 | } 587 | 588 | Modal.prototype.toggle = function (_relatedTarget) { 589 | return this[!this.isShown ? 'show' : 'hide'](_relatedTarget) 590 | } 591 | 592 | Modal.prototype.show = function (_relatedTarget) { 593 | var that = this 594 | var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) 595 | 596 | this.$element.trigger(e) 597 | 598 | if (this.isShown || e.isDefaultPrevented()) return 599 | 600 | this.isShown = true 601 | 602 | this.escape() 603 | 604 | this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) 605 | 606 | this.backdrop(function () { 607 | var transition = $.support.transition && that.$element.hasClass('fade') 608 | 609 | if (!that.$element.parent().length) { 610 | that.$element.appendTo(document.body) // don't move modals dom position 611 | } 612 | 613 | that.$element 614 | .show() 615 | .scrollTop(0) 616 | 617 | if (transition) { 618 | that.$element[0].offsetWidth // force reflow 619 | } 620 | 621 | that.$element 622 | .addClass('in') 623 | .attr('aria-hidden', false) 624 | 625 | that.enforceFocus() 626 | 627 | var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) 628 | 629 | transition ? 630 | that.$element.find('.modal-dialog') // wait for modal to slide in 631 | .one($.support.transition.end, function () { 632 | that.$element.focus().trigger(e) 633 | }) 634 | .emulateTransitionEnd(300) : 635 | that.$element.focus().trigger(e) 636 | }) 637 | } 638 | 639 | Modal.prototype.hide = function (e) { 640 | if (e) e.preventDefault() 641 | 642 | e = $.Event('hide.bs.modal') 643 | 644 | this.$element.trigger(e) 645 | 646 | if (!this.isShown || e.isDefaultPrevented()) return 647 | 648 | this.isShown = false 649 | 650 | this.escape() 651 | 652 | $(document).off('focusin.bs.modal') 653 | 654 | this.$element 655 | .removeClass('in') 656 | .attr('aria-hidden', true) 657 | .off('click.dismiss.bs.modal') 658 | 659 | $.support.transition && this.$element.hasClass('fade') ? 660 | this.$element 661 | .one($.support.transition.end, $.proxy(this.hideModal, this)) 662 | .emulateTransitionEnd(300) : 663 | this.hideModal() 664 | } 665 | 666 | Modal.prototype.enforceFocus = function () { 667 | $(document) 668 | .off('focusin.bs.modal') // guard against infinite focus loop 669 | .on('focusin.bs.modal', $.proxy(function (e) { 670 | if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { 671 | this.$element.focus() 672 | } 673 | }, this)) 674 | } 675 | 676 | Modal.prototype.escape = function () { 677 | if (this.isShown && this.options.keyboard) { 678 | this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) { 679 | e.which == 27 && this.hide() 680 | }, this)) 681 | } else if (!this.isShown) { 682 | this.$element.off('keyup.dismiss.bs.modal') 683 | } 684 | } 685 | 686 | Modal.prototype.hideModal = function () { 687 | var that = this 688 | this.$element.hide() 689 | this.backdrop(function () { 690 | that.removeBackdrop() 691 | that.$element.trigger('hidden.bs.modal') 692 | }) 693 | } 694 | 695 | Modal.prototype.removeBackdrop = function () { 696 | this.$backdrop && this.$backdrop.remove() 697 | this.$backdrop = null 698 | } 699 | 700 | Modal.prototype.backdrop = function (callback) { 701 | var animate = this.$element.hasClass('fade') ? 'fade' : '' 702 | 703 | if (this.isShown && this.options.backdrop) { 704 | var doAnimate = $.support.transition && animate 705 | 706 | this.$backdrop = $('') 707 | .appendTo(document.body) 708 | 709 | this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { 710 | if (e.target !== e.currentTarget) return 711 | this.options.backdrop == 'static' 712 | ? this.$element[0].focus.call(this.$element[0]) 713 | : this.hide.call(this) 714 | }, this)) 715 | 716 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow 717 | 718 | this.$backdrop.addClass('in') 719 | 720 | if (!callback) return 721 | 722 | doAnimate ? 723 | this.$backdrop 724 | .one($.support.transition.end, callback) 725 | .emulateTransitionEnd(150) : 726 | callback() 727 | 728 | } else if (!this.isShown && this.$backdrop) { 729 | this.$backdrop.removeClass('in') 730 | 731 | $.support.transition && this.$element.hasClass('fade') ? 732 | this.$backdrop 733 | .one($.support.transition.end, callback) 734 | .emulateTransitionEnd(150) : 735 | callback() 736 | 737 | } else if (callback) { 738 | callback() 739 | } 740 | } 741 | 742 | 743 | // MODAL PLUGIN DEFINITION 744 | // ======================= 745 | 746 | var old = $.fn.modal 747 | 748 | $.fn.modal = function (option, _relatedTarget) { 749 | return this.each(function () { 750 | var $this = $(this) 751 | var data = $this.data('bs.modal') 752 | var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) 753 | 754 | if (!data) $this.data('bs.modal', (data = new Modal(this, options))) 755 | if (typeof option == 'string') data[option](_relatedTarget) 756 | else if (options.show) data.show(_relatedTarget) 757 | }) 758 | } 759 | 760 | $.fn.modal.Constructor = Modal 761 | 762 | 763 | // MODAL NO CONFLICT 764 | // ================= 765 | 766 | $.fn.modal.noConflict = function () { 767 | $.fn.modal = old 768 | return this 769 | } 770 | 771 | 772 | // MODAL DATA-API 773 | // ============== 774 | 775 | $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { 776 | var $this = $(this) 777 | var href = $this.attr('href') 778 | var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 779 | var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) 780 | 781 | if ($this.is('a')) e.preventDefault() 782 | 783 | $target 784 | .modal(option, this) 785 | .one('hide', function () { 786 | $this.is(':visible') && $this.focus() 787 | }) 788 | }) 789 | 790 | $(document) 791 | .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') }) 792 | .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') }) 793 | 794 | }(jQuery); 795 | 796 | /* ======================================================================== 797 | * Bootstrap: tooltip.js v3.1.1 798 | * http://getbootstrap.com/javascript/#tooltip 799 | * Inspired by the original jQuery.tipsy by Jason Frame 800 | * ======================================================================== 801 | * Copyright 2011-2014 Twitter, Inc. 802 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 803 | * ======================================================================== */ 804 | 805 | 806 | +function ($) { 807 | 'use strict'; 808 | 809 | // TOOLTIP PUBLIC CLASS DEFINITION 810 | // =============================== 811 | 812 | var Tooltip = function (element, options) { 813 | this.type = 814 | this.options = 815 | this.enabled = 816 | this.timeout = 817 | this.hoverState = 818 | this.$element = null 819 | 820 | this.init('tooltip', element, options) 821 | } 822 | 823 | Tooltip.DEFAULTS = { 824 | animation: true, 825 | placement: 'top', 826 | selector: false, 827 | template: '', 828 | trigger: 'hover focus', 829 | title: '', 830 | delay: 0, 831 | html: false, 832 | container: false 833 | } 834 | 835 | Tooltip.prototype.init = function (type, element, options) { 836 | this.enabled = true 837 | this.type = type 838 | this.$element = $(element) 839 | this.options = this.getOptions(options) 840 | 841 | var triggers = this.options.trigger.split(' ') 842 | 843 | for (var i = triggers.length; i--;) { 844 | var trigger = triggers[i] 845 | 846 | if (trigger == 'click') { 847 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) 848 | } else if (trigger != 'manual') { 849 | var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' 850 | var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' 851 | 852 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) 853 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) 854 | } 855 | } 856 | 857 | this.options.selector ? 858 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : 859 | this.fixTitle() 860 | } 861 | 862 | Tooltip.prototype.getDefaults = function () { 863 | return Tooltip.DEFAULTS 864 | } 865 | 866 | Tooltip.prototype.getOptions = function (options) { 867 | options = $.extend({}, this.getDefaults(), this.$element.data(), options) 868 | 869 | if (options.delay && typeof options.delay == 'number') { 870 | options.delay = { 871 | show: options.delay, 872 | hide: options.delay 873 | } 874 | } 875 | 876 | return options 877 | } 878 | 879 | Tooltip.prototype.getDelegateOptions = function () { 880 | var options = {} 881 | var defaults = this.getDefaults() 882 | 883 | this._options && $.each(this._options, function (key, value) { 884 | if (defaults[key] != value) options[key] = value 885 | }) 886 | 887 | return options 888 | } 889 | 890 | Tooltip.prototype.enter = function (obj) { 891 | var self = obj instanceof this.constructor ? 892 | obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) 893 | 894 | clearTimeout(self.timeout) 895 | 896 | self.hoverState = 'in' 897 | 898 | if (!self.options.delay || !self.options.delay.show) return self.show() 899 | 900 | self.timeout = setTimeout(function () { 901 | if (self.hoverState == 'in') self.show() 902 | }, self.options.delay.show) 903 | } 904 | 905 | Tooltip.prototype.leave = function (obj) { 906 | var self = obj instanceof this.constructor ? 907 | obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) 908 | 909 | clearTimeout(self.timeout) 910 | 911 | self.hoverState = 'out' 912 | 913 | if (!self.options.delay || !self.options.delay.hide) return self.hide() 914 | 915 | self.timeout = setTimeout(function () { 916 | if (self.hoverState == 'out') self.hide() 917 | }, self.options.delay.hide) 918 | } 919 | 920 | Tooltip.prototype.show = function () { 921 | var e = $.Event('show.bs.' + this.type) 922 | 923 | if (this.hasContent() && this.enabled) { 924 | this.$element.trigger(e) 925 | 926 | if (e.isDefaultPrevented()) return 927 | var that = this; 928 | 929 | var $tip = this.tip() 930 | 931 | this.setContent() 932 | 933 | if (this.options.animation) $tip.addClass('fade') 934 | 935 | var placement = typeof this.options.placement == 'function' ? 936 | this.options.placement.call(this, $tip[0], this.$element[0]) : 937 | this.options.placement 938 | 939 | var autoToken = /\s?auto?\s?/i 940 | var autoPlace = autoToken.test(placement) 941 | if (autoPlace) placement = placement.replace(autoToken, '') || 'top' 942 | 943 | $tip 944 | .detach() 945 | .css({ top: 0, left: 0, display: 'block' }) 946 | .addClass(placement) 947 | 948 | this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) 949 | 950 | var pos = this.getPosition() 951 | var actualWidth = $tip[0].offsetWidth 952 | var actualHeight = $tip[0].offsetHeight 953 | 954 | if (autoPlace) { 955 | var $parent = this.$element.parent() 956 | 957 | var orgPlacement = placement 958 | var docScroll = document.documentElement.scrollTop || document.body.scrollTop 959 | var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth() 960 | var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight() 961 | var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left 962 | 963 | placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' : 964 | placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' : 965 | placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' : 966 | placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' : 967 | placement 968 | 969 | $tip 970 | .removeClass(orgPlacement) 971 | .addClass(placement) 972 | } 973 | 974 | var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) 975 | 976 | this.applyPlacement(calculatedOffset, placement) 977 | this.hoverState = null 978 | 979 | var complete = function() { 980 | that.$element.trigger('shown.bs.' + that.type) 981 | } 982 | 983 | $.support.transition && this.$tip.hasClass('fade') ? 984 | $tip 985 | .one($.support.transition.end, complete) 986 | .emulateTransitionEnd(150) : 987 | complete() 988 | } 989 | } 990 | 991 | Tooltip.prototype.applyPlacement = function (offset, placement) { 992 | var replace 993 | var $tip = this.tip() 994 | var width = $tip[0].offsetWidth 995 | var height = $tip[0].offsetHeight 996 | 997 | // manually read margins because getBoundingClientRect includes difference 998 | var marginTop = parseInt($tip.css('margin-top'), 10) 999 | var marginLeft = parseInt($tip.css('margin-left'), 10) 1000 | 1001 | // we must check for NaN for ie 8/9 1002 | if (isNaN(marginTop)) marginTop = 0 1003 | if (isNaN(marginLeft)) marginLeft = 0 1004 | 1005 | offset.top = offset.top + marginTop 1006 | offset.left = offset.left + marginLeft 1007 | 1008 | // $.fn.offset doesn't round pixel values 1009 | // so we use setOffset directly with our own function B-0 1010 | $.offset.setOffset($tip[0], $.extend({ 1011 | using: function (props) { 1012 | $tip.css({ 1013 | top: Math.round(props.top), 1014 | left: Math.round(props.left) 1015 | }) 1016 | } 1017 | }, offset), 0) 1018 | 1019 | $tip.addClass('in') 1020 | 1021 | // check to see if placing tip in new offset caused the tip to resize itself 1022 | var actualWidth = $tip[0].offsetWidth 1023 | var actualHeight = $tip[0].offsetHeight 1024 | 1025 | if (placement == 'top' && actualHeight != height) { 1026 | replace = true 1027 | offset.top = offset.top + height - actualHeight 1028 | } 1029 | 1030 | if (/bottom|top/.test(placement)) { 1031 | var delta = 0 1032 | 1033 | if (offset.left < 0) { 1034 | delta = offset.left * -2 1035 | offset.left = 0 1036 | 1037 | $tip.offset(offset) 1038 | 1039 | actualWidth = $tip[0].offsetWidth 1040 | actualHeight = $tip[0].offsetHeight 1041 | } 1042 | 1043 | this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') 1044 | } else { 1045 | this.replaceArrow(actualHeight - height, actualHeight, 'top') 1046 | } 1047 | 1048 | if (replace) $tip.offset(offset) 1049 | } 1050 | 1051 | Tooltip.prototype.replaceArrow = function (delta, dimension, position) { 1052 | this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '') 1053 | } 1054 | 1055 | Tooltip.prototype.setContent = function () { 1056 | var $tip = this.tip() 1057 | var title = this.getTitle() 1058 | 1059 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) 1060 | $tip.removeClass('fade in top bottom left right') 1061 | } 1062 | 1063 | Tooltip.prototype.hide = function () { 1064 | var that = this 1065 | var $tip = this.tip() 1066 | var e = $.Event('hide.bs.' + this.type) 1067 | 1068 | function complete() { 1069 | if (that.hoverState != 'in') $tip.detach() 1070 | that.$element.trigger('hidden.bs.' + that.type) 1071 | } 1072 | 1073 | this.$element.trigger(e) 1074 | 1075 | if (e.isDefaultPrevented()) return 1076 | 1077 | $tip.removeClass('in') 1078 | 1079 | $.support.transition && this.$tip.hasClass('fade') ? 1080 | $tip 1081 | .one($.support.transition.end, complete) 1082 | .emulateTransitionEnd(150) : 1083 | complete() 1084 | 1085 | this.hoverState = null 1086 | 1087 | return this 1088 | } 1089 | 1090 | Tooltip.prototype.fixTitle = function () { 1091 | var $e = this.$element 1092 | if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { 1093 | $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') 1094 | } 1095 | } 1096 | 1097 | Tooltip.prototype.hasContent = function () { 1098 | return this.getTitle() 1099 | } 1100 | 1101 | Tooltip.prototype.getPosition = function () { 1102 | var el = this.$element[0] 1103 | return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { 1104 | width: el.offsetWidth, 1105 | height: el.offsetHeight 1106 | }, this.$element.offset()) 1107 | } 1108 | 1109 | Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { 1110 | return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : 1111 | placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : 1112 | placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : 1113 | /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } 1114 | } 1115 | 1116 | Tooltip.prototype.getTitle = function () { 1117 | var title 1118 | var $e = this.$element 1119 | var o = this.options 1120 | 1121 | title = $e.attr('data-original-title') 1122 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 1123 | 1124 | return title 1125 | } 1126 | 1127 | Tooltip.prototype.tip = function () { 1128 | return this.$tip = this.$tip || $(this.options.template) 1129 | } 1130 | 1131 | Tooltip.prototype.arrow = function () { 1132 | return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow') 1133 | } 1134 | 1135 | Tooltip.prototype.validate = function () { 1136 | if (!this.$element[0].parentNode) { 1137 | this.hide() 1138 | this.$element = null 1139 | this.options = null 1140 | } 1141 | } 1142 | 1143 | Tooltip.prototype.enable = function () { 1144 | this.enabled = true 1145 | } 1146 | 1147 | Tooltip.prototype.disable = function () { 1148 | this.enabled = false 1149 | } 1150 | 1151 | Tooltip.prototype.toggleEnabled = function () { 1152 | this.enabled = !this.enabled 1153 | } 1154 | 1155 | Tooltip.prototype.toggle = function (e) { 1156 | var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this 1157 | self.tip().hasClass('in') ? self.leave(self) : self.enter(self) 1158 | } 1159 | 1160 | Tooltip.prototype.destroy = function () { 1161 | clearTimeout(this.timeout) 1162 | this.hide().$element.off('.' + this.type).removeData('bs.' + this.type) 1163 | } 1164 | 1165 | 1166 | // TOOLTIP PLUGIN DEFINITION 1167 | // ========================= 1168 | 1169 | var old = $.fn.tooltip 1170 | 1171 | $.fn.tooltip = function (option) { 1172 | return this.each(function () { 1173 | var $this = $(this) 1174 | var data = $this.data('bs.tooltip') 1175 | var options = typeof option == 'object' && option 1176 | 1177 | if (!data && option == 'destroy') return 1178 | if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) 1179 | if (typeof option == 'string') data[option]() 1180 | }) 1181 | } 1182 | 1183 | $.fn.tooltip.Constructor = Tooltip 1184 | 1185 | 1186 | // TOOLTIP NO CONFLICT 1187 | // =================== 1188 | 1189 | $.fn.tooltip.noConflict = function () { 1190 | $.fn.tooltip = old 1191 | return this 1192 | } 1193 | 1194 | }(jQuery); 1195 | 1196 | /* ======================================================================== 1197 | * Bootstrap: popover.js v3.1.1 1198 | * http://getbootstrap.com/javascript/#popovers 1199 | * ======================================================================== 1200 | * Copyright 2011-2014 Twitter, Inc. 1201 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1202 | * ======================================================================== */ 1203 | 1204 | 1205 | +function ($) { 1206 | 'use strict'; 1207 | 1208 | // POPOVER PUBLIC CLASS DEFINITION 1209 | // =============================== 1210 | 1211 | var Popover = function (element, options) { 1212 | this.init('popover', element, options) 1213 | } 1214 | 1215 | if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') 1216 | 1217 | Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { 1218 | placement: 'right', 1219 | trigger: 'click', 1220 | content: '', 1221 | template: '' 1222 | }) 1223 | 1224 | 1225 | // NOTE: POPOVER EXTENDS tooltip.js 1226 | // ================================ 1227 | 1228 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) 1229 | 1230 | Popover.prototype.constructor = Popover 1231 | 1232 | Popover.prototype.getDefaults = function () { 1233 | return Popover.DEFAULTS 1234 | } 1235 | 1236 | Popover.prototype.setContent = function () { 1237 | var $tip = this.tip() 1238 | var title = this.getTitle() 1239 | var content = this.getContent() 1240 | 1241 | $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) 1242 | $tip.find('.popover-content')[ // we use append for html objects to maintain js events 1243 | this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' 1244 | ](content) 1245 | 1246 | $tip.removeClass('fade top bottom left right in') 1247 | 1248 | // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do 1249 | // this manually by checking the contents. 1250 | if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() 1251 | } 1252 | 1253 | Popover.prototype.hasContent = function () { 1254 | return this.getTitle() || this.getContent() 1255 | } 1256 | 1257 | Popover.prototype.getContent = function () { 1258 | var $e = this.$element 1259 | var o = this.options 1260 | 1261 | return $e.attr('data-content') 1262 | || (typeof o.content == 'function' ? 1263 | o.content.call($e[0]) : 1264 | o.content) 1265 | } 1266 | 1267 | Popover.prototype.arrow = function () { 1268 | return this.$arrow = this.$arrow || this.tip().find('.arrow') 1269 | } 1270 | 1271 | Popover.prototype.tip = function () { 1272 | if (!this.$tip) this.$tip = $(this.options.template) 1273 | return this.$tip 1274 | } 1275 | 1276 | 1277 | // POPOVER PLUGIN DEFINITION 1278 | // ========================= 1279 | 1280 | var old = $.fn.popover 1281 | 1282 | $.fn.popover = function (option) { 1283 | return this.each(function () { 1284 | var $this = $(this) 1285 | var data = $this.data('bs.popover') 1286 | var options = typeof option == 'object' && option 1287 | 1288 | if (!data && option == 'destroy') return 1289 | if (!data) $this.data('bs.popover', (data = new Popover(this, options))) 1290 | if (typeof option == 'string') data[option]() 1291 | }) 1292 | } 1293 | 1294 | $.fn.popover.Constructor = Popover 1295 | 1296 | 1297 | // POPOVER NO CONFLICT 1298 | // =================== 1299 | 1300 | $.fn.popover.noConflict = function () { 1301 | $.fn.popover = old 1302 | return this 1303 | } 1304 | 1305 | }(jQuery); 1306 | 1307 | /* ======================================================================== 1308 | * Bootstrap: tab.js v3.1.1 1309 | * http://getbootstrap.com/javascript/#tabs 1310 | * ======================================================================== 1311 | * Copyright 2011-2014 Twitter, Inc. 1312 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1313 | * ======================================================================== */ 1314 | 1315 | 1316 | +function ($) { 1317 | 'use strict'; 1318 | 1319 | // TAB CLASS DEFINITION 1320 | // ==================== 1321 | 1322 | var Tab = function (element) { 1323 | this.element = $(element) 1324 | } 1325 | 1326 | Tab.prototype.show = function () { 1327 | var $this = this.element 1328 | var $ul = $this.closest('ul:not(.dropdown-menu)') 1329 | var selector = $this.data('target') 1330 | 1331 | if (!selector) { 1332 | selector = $this.attr('href') 1333 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 1334 | } 1335 | 1336 | if ($this.parent('li').hasClass('active')) return 1337 | 1338 | var previous = $ul.find('.active:last a')[0] 1339 | var e = $.Event('show.bs.tab', { 1340 | relatedTarget: previous 1341 | }) 1342 | 1343 | $this.trigger(e) 1344 | 1345 | if (e.isDefaultPrevented()) return 1346 | 1347 | var $target = $(selector) 1348 | 1349 | this.activate($this.parent('li'), $ul) 1350 | this.activate($target, $target.parent(), function () { 1351 | $this.trigger({ 1352 | type: 'shown.bs.tab', 1353 | relatedTarget: previous 1354 | }) 1355 | }) 1356 | } 1357 | 1358 | Tab.prototype.activate = function (element, container, callback) { 1359 | var $active = container.find('> .active') 1360 | var transition = callback 1361 | && $.support.transition 1362 | && $active.hasClass('fade') 1363 | 1364 | function next() { 1365 | $active 1366 | .removeClass('active') 1367 | .find('> .dropdown-menu > .active') 1368 | .removeClass('active') 1369 | 1370 | element.addClass('active') 1371 | 1372 | if (transition) { 1373 | element[0].offsetWidth // reflow for transition 1374 | element.addClass('in') 1375 | } else { 1376 | element.removeClass('fade') 1377 | } 1378 | 1379 | if (element.parent('.dropdown-menu')) { 1380 | element.closest('li.dropdown').addClass('active') 1381 | } 1382 | 1383 | callback && callback() 1384 | } 1385 | 1386 | transition ? 1387 | $active 1388 | .one($.support.transition.end, next) 1389 | .emulateTransitionEnd(150) : 1390 | next() 1391 | 1392 | $active.removeClass('in') 1393 | } 1394 | 1395 | 1396 | // TAB PLUGIN DEFINITION 1397 | // ===================== 1398 | 1399 | var old = $.fn.tab 1400 | 1401 | $.fn.tab = function ( option ) { 1402 | return this.each(function () { 1403 | var $this = $(this) 1404 | var data = $this.data('bs.tab') 1405 | 1406 | if (!data) $this.data('bs.tab', (data = new Tab(this))) 1407 | if (typeof option == 'string') data[option]() 1408 | }) 1409 | } 1410 | 1411 | $.fn.tab.Constructor = Tab 1412 | 1413 | 1414 | // TAB NO CONFLICT 1415 | // =============== 1416 | 1417 | $.fn.tab.noConflict = function () { 1418 | $.fn.tab = old 1419 | return this 1420 | } 1421 | 1422 | 1423 | // TAB DATA-API 1424 | // ============ 1425 | 1426 | $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { 1427 | e.preventDefault() 1428 | $(this).tab('show') 1429 | }) 1430 | 1431 | }(jQuery); 1432 | 1433 | /* ======================================================================== 1434 | * Bootstrap: affix.js v3.1.1 1435 | * http://getbootstrap.com/javascript/#affix 1436 | * ======================================================================== 1437 | * Copyright 2011-2014 Twitter, Inc. 1438 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1439 | * ======================================================================== */ 1440 | 1441 | 1442 | +function ($) { 1443 | 'use strict'; 1444 | 1445 | // AFFIX CLASS DEFINITION 1446 | // ====================== 1447 | 1448 | var Affix = function (element, options) { 1449 | this.options = $.extend({}, Affix.DEFAULTS, options) 1450 | this.$window = $(window) 1451 | .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) 1452 | .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) 1453 | 1454 | this.$element = $(element) 1455 | this.affixed = 1456 | this.unpin = 1457 | this.pinnedOffset = null 1458 | 1459 | this.checkPosition() 1460 | } 1461 | 1462 | Affix.RESET = 'affix affix-top affix-bottom' 1463 | 1464 | Affix.DEFAULTS = { 1465 | offset: 0 1466 | } 1467 | 1468 | Affix.prototype.getPinnedOffset = function () { 1469 | if (this.pinnedOffset) return this.pinnedOffset 1470 | this.$element.removeClass(Affix.RESET).addClass('affix') 1471 | var scrollTop = this.$window.scrollTop() 1472 | var position = this.$element.offset() 1473 | return (this.pinnedOffset = position.top - scrollTop) 1474 | } 1475 | 1476 | Affix.prototype.checkPositionWithEventLoop = function () { 1477 | setTimeout($.proxy(this.checkPosition, this), 1) 1478 | } 1479 | 1480 | Affix.prototype.checkPosition = function () { 1481 | if (!this.$element.is(':visible')) return 1482 | 1483 | var scrollHeight = $(document).height() 1484 | var scrollTop = this.$window.scrollTop() 1485 | var position = this.$element.offset() 1486 | var offset = this.options.offset 1487 | var offsetTop = offset.top 1488 | var offsetBottom = offset.bottom 1489 | 1490 | if (this.affixed == 'top') position.top += scrollTop 1491 | 1492 | if (typeof offset != 'object') offsetBottom = offsetTop = offset 1493 | if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) 1494 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) 1495 | 1496 | var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : 1497 | offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : 1498 | offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false 1499 | 1500 | if (this.affixed === affix) return 1501 | if (this.unpin) this.$element.css('top', '') 1502 | 1503 | var affixType = 'affix' + (affix ? '-' + affix : '') 1504 | var e = $.Event(affixType + '.bs.affix') 1505 | 1506 | this.$element.trigger(e) 1507 | 1508 | if (e.isDefaultPrevented()) return 1509 | 1510 | this.affixed = affix 1511 | this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null 1512 | 1513 | this.$element 1514 | .removeClass(Affix.RESET) 1515 | .addClass(affixType) 1516 | .trigger($.Event(affixType.replace('affix', 'affixed'))) 1517 | 1518 | if (affix == 'bottom') { 1519 | this.$element.offset({ top: scrollHeight - offsetBottom - this.$element.height() }) 1520 | } 1521 | } 1522 | 1523 | 1524 | // AFFIX PLUGIN DEFINITION 1525 | // ======================= 1526 | 1527 | var old = $.fn.affix 1528 | 1529 | $.fn.affix = function (option) { 1530 | return this.each(function () { 1531 | var $this = $(this) 1532 | var data = $this.data('bs.affix') 1533 | var options = typeof option == 'object' && option 1534 | 1535 | if (!data) $this.data('bs.affix', (data = new Affix(this, options))) 1536 | if (typeof option == 'string') data[option]() 1537 | }) 1538 | } 1539 | 1540 | $.fn.affix.Constructor = Affix 1541 | 1542 | 1543 | // AFFIX NO CONFLICT 1544 | // ================= 1545 | 1546 | $.fn.affix.noConflict = function () { 1547 | $.fn.affix = old 1548 | return this 1549 | } 1550 | 1551 | 1552 | // AFFIX DATA-API 1553 | // ============== 1554 | 1555 | $(window).on('load', function () { 1556 | $('[data-spy="affix"]').each(function () { 1557 | var $spy = $(this) 1558 | var data = $spy.data() 1559 | 1560 | data.offset = data.offset || {} 1561 | 1562 | if (data.offsetBottom) data.offset.bottom = data.offsetBottom 1563 | if (data.offsetTop) data.offset.top = data.offsetTop 1564 | 1565 | $spy.affix(data) 1566 | }) 1567 | }) 1568 | 1569 | }(jQuery); 1570 | 1571 | /* ======================================================================== 1572 | * Bootstrap: collapse.js v3.1.1 1573 | * http://getbootstrap.com/javascript/#collapse 1574 | * ======================================================================== 1575 | * Copyright 2011-2014 Twitter, Inc. 1576 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1577 | * ======================================================================== */ 1578 | 1579 | 1580 | +function ($) { 1581 | 'use strict'; 1582 | 1583 | // COLLAPSE PUBLIC CLASS DEFINITION 1584 | // ================================ 1585 | 1586 | var Collapse = function (element, options) { 1587 | this.$element = $(element) 1588 | this.options = $.extend({}, Collapse.DEFAULTS, options) 1589 | this.transitioning = null 1590 | 1591 | if (this.options.parent) this.$parent = $(this.options.parent) 1592 | if (this.options.toggle) this.toggle() 1593 | } 1594 | 1595 | Collapse.DEFAULTS = { 1596 | toggle: true 1597 | } 1598 | 1599 | Collapse.prototype.dimension = function () { 1600 | var hasWidth = this.$element.hasClass('width') 1601 | return hasWidth ? 'width' : 'height' 1602 | } 1603 | 1604 | Collapse.prototype.show = function () { 1605 | if (this.transitioning || this.$element.hasClass('in')) return 1606 | 1607 | var startEvent = $.Event('show.bs.collapse') 1608 | this.$element.trigger(startEvent) 1609 | if (startEvent.isDefaultPrevented()) return 1610 | 1611 | var actives = this.$parent && this.$parent.find('> .panel > .in') 1612 | 1613 | if (actives && actives.length) { 1614 | var hasData = actives.data('bs.collapse') 1615 | if (hasData && hasData.transitioning) return 1616 | actives.collapse('hide') 1617 | hasData || actives.data('bs.collapse', null) 1618 | } 1619 | 1620 | var dimension = this.dimension() 1621 | 1622 | this.$element 1623 | .removeClass('collapse') 1624 | .addClass('collapsing') 1625 | [dimension](0) 1626 | 1627 | this.transitioning = 1 1628 | 1629 | var complete = function () { 1630 | this.$element 1631 | .removeClass('collapsing') 1632 | .addClass('collapse in') 1633 | [dimension]('auto') 1634 | this.transitioning = 0 1635 | this.$element.trigger('shown.bs.collapse') 1636 | } 1637 | 1638 | if (!$.support.transition) return complete.call(this) 1639 | 1640 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 1641 | 1642 | this.$element 1643 | .one($.support.transition.end, $.proxy(complete, this)) 1644 | .emulateTransitionEnd(350) 1645 | [dimension](this.$element[0][scrollSize]) 1646 | } 1647 | 1648 | Collapse.prototype.hide = function () { 1649 | if (this.transitioning || !this.$element.hasClass('in')) return 1650 | 1651 | var startEvent = $.Event('hide.bs.collapse') 1652 | this.$element.trigger(startEvent) 1653 | if (startEvent.isDefaultPrevented()) return 1654 | 1655 | var dimension = this.dimension() 1656 | 1657 | this.$element 1658 | [dimension](this.$element[dimension]()) 1659 | [0].offsetHeight 1660 | 1661 | this.$element 1662 | .addClass('collapsing') 1663 | .removeClass('collapse') 1664 | .removeClass('in') 1665 | 1666 | this.transitioning = 1 1667 | 1668 | var complete = function () { 1669 | this.transitioning = 0 1670 | this.$element 1671 | .trigger('hidden.bs.collapse') 1672 | .removeClass('collapsing') 1673 | .addClass('collapse') 1674 | } 1675 | 1676 | if (!$.support.transition) return complete.call(this) 1677 | 1678 | this.$element 1679 | [dimension](0) 1680 | .one($.support.transition.end, $.proxy(complete, this)) 1681 | .emulateTransitionEnd(350) 1682 | } 1683 | 1684 | Collapse.prototype.toggle = function () { 1685 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 1686 | } 1687 | 1688 | 1689 | // COLLAPSE PLUGIN DEFINITION 1690 | // ========================== 1691 | 1692 | var old = $.fn.collapse 1693 | 1694 | $.fn.collapse = function (option) { 1695 | return this.each(function () { 1696 | var $this = $(this) 1697 | var data = $this.data('bs.collapse') 1698 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 1699 | 1700 | if (!data && options.toggle && option == 'show') option = !option 1701 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 1702 | if (typeof option == 'string') data[option]() 1703 | }) 1704 | } 1705 | 1706 | $.fn.collapse.Constructor = Collapse 1707 | 1708 | 1709 | // COLLAPSE NO CONFLICT 1710 | // ==================== 1711 | 1712 | $.fn.collapse.noConflict = function () { 1713 | $.fn.collapse = old 1714 | return this 1715 | } 1716 | 1717 | 1718 | // COLLAPSE DATA-API 1719 | // ================= 1720 | 1721 | $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { 1722 | var $this = $(this), href 1723 | var target = $this.attr('data-target') 1724 | || e.preventDefault() 1725 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 1726 | var $target = $(target) 1727 | var data = $target.data('bs.collapse') 1728 | var option = data ? 'toggle' : $this.data() 1729 | var parent = $this.attr('data-parent') 1730 | var $parent = parent && $(parent) 1731 | 1732 | if (!data || !data.transitioning) { 1733 | if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') 1734 | $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') 1735 | } 1736 | 1737 | $target.collapse(option) 1738 | }) 1739 | 1740 | }(jQuery); 1741 | 1742 | /* ======================================================================== 1743 | * Bootstrap: scrollspy.js v3.1.1 1744 | * http://getbootstrap.com/javascript/#scrollspy 1745 | * ======================================================================== 1746 | * Copyright 2011-2014 Twitter, Inc. 1747 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1748 | * ======================================================================== */ 1749 | 1750 | 1751 | +function ($) { 1752 | 'use strict'; 1753 | 1754 | // SCROLLSPY CLASS DEFINITION 1755 | // ========================== 1756 | 1757 | function ScrollSpy(element, options) { 1758 | var href 1759 | var process = $.proxy(this.process, this) 1760 | 1761 | this.$element = $(element).is('body') ? $(window) : $(element) 1762 | this.$body = $('body') 1763 | this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process) 1764 | this.options = $.extend({}, ScrollSpy.DEFAULTS, options) 1765 | this.selector = (this.options.target 1766 | || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 1767 | || '') + ' .nav li > a' 1768 | this.offsets = $([]) 1769 | this.targets = $([]) 1770 | this.activeTarget = null 1771 | 1772 | this.refresh() 1773 | this.process() 1774 | } 1775 | 1776 | ScrollSpy.DEFAULTS = { 1777 | offset: 10 1778 | } 1779 | 1780 | ScrollSpy.prototype.refresh = function () { 1781 | var offsetMethod = this.$element[0] == window ? 'offset' : 'position' 1782 | 1783 | this.offsets = $([]) 1784 | this.targets = $([]) 1785 | 1786 | var self = this 1787 | var $targets = this.$body 1788 | .find(this.selector) 1789 | .map(function () { 1790 | var $el = $(this) 1791 | var href = $el.data('target') || $el.attr('href') 1792 | var $href = /^#./.test(href) && $(href) 1793 | 1794 | return ($href 1795 | && $href.length 1796 | && $href.is(':visible') 1797 | && [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null 1798 | }) 1799 | .sort(function (a, b) { return a[0] - b[0] }) 1800 | .each(function () { 1801 | self.offsets.push(this[0]) 1802 | self.targets.push(this[1]) 1803 | }) 1804 | } 1805 | 1806 | ScrollSpy.prototype.process = function () { 1807 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset 1808 | var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight 1809 | var maxScroll = scrollHeight - this.$scrollElement.height() 1810 | var offsets = this.offsets 1811 | var targets = this.targets 1812 | var activeTarget = this.activeTarget 1813 | var i 1814 | 1815 | if (scrollTop >= maxScroll) { 1816 | return activeTarget != (i = targets.last()[0]) && this.activate(i) 1817 | } 1818 | 1819 | if (activeTarget && scrollTop <= offsets[0]) { 1820 | return activeTarget != (i = targets[0]) && this.activate(i) 1821 | } 1822 | 1823 | for (i = offsets.length; i--;) { 1824 | activeTarget != targets[i] 1825 | && scrollTop >= offsets[i] 1826 | && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) 1827 | && this.activate( targets[i] ) 1828 | } 1829 | } 1830 | 1831 | ScrollSpy.prototype.activate = function (target) { 1832 | this.activeTarget = target 1833 | 1834 | $(this.selector) 1835 | .parentsUntil(this.options.target, '.active') 1836 | .removeClass('active') 1837 | 1838 | var selector = this.selector + 1839 | '[data-target="' + target + '"],' + 1840 | this.selector + '[href="' + target + '"]' 1841 | 1842 | var active = $(selector) 1843 | .parents('li') 1844 | .addClass('active') 1845 | 1846 | if (active.parent('.dropdown-menu').length) { 1847 | active = active 1848 | .closest('li.dropdown') 1849 | .addClass('active') 1850 | } 1851 | 1852 | active.trigger('activate.bs.scrollspy') 1853 | } 1854 | 1855 | 1856 | // SCROLLSPY PLUGIN DEFINITION 1857 | // =========================== 1858 | 1859 | var old = $.fn.scrollspy 1860 | 1861 | $.fn.scrollspy = function (option) { 1862 | return this.each(function () { 1863 | var $this = $(this) 1864 | var data = $this.data('bs.scrollspy') 1865 | var options = typeof option == 'object' && option 1866 | 1867 | if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) 1868 | if (typeof option == 'string') data[option]() 1869 | }) 1870 | } 1871 | 1872 | $.fn.scrollspy.Constructor = ScrollSpy 1873 | 1874 | 1875 | // SCROLLSPY NO CONFLICT 1876 | // ===================== 1877 | 1878 | $.fn.scrollspy.noConflict = function () { 1879 | $.fn.scrollspy = old 1880 | return this 1881 | } 1882 | 1883 | 1884 | // SCROLLSPY DATA-API 1885 | // ================== 1886 | 1887 | $(window).on('load', function () { 1888 | $('[data-spy="scroll"]').each(function () { 1889 | var $spy = $(this) 1890 | $spy.scrollspy($spy.data()) 1891 | }) 1892 | }) 1893 | 1894 | }(jQuery); 1895 | 1896 | /* ======================================================================== 1897 | * Bootstrap: transition.js v3.1.1 1898 | * http://getbootstrap.com/javascript/#transitions 1899 | * ======================================================================== 1900 | * Copyright 2011-2014 Twitter, Inc. 1901 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1902 | * ======================================================================== */ 1903 | 1904 | 1905 | +function ($) { 1906 | 'use strict'; 1907 | 1908 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 1909 | // ============================================================ 1910 | 1911 | function transitionEnd() { 1912 | var el = document.createElement('bootstrap') 1913 | 1914 | var transEndEventNames = { 1915 | 'WebkitTransition' : 'webkitTransitionEnd', 1916 | 'MozTransition' : 'transitionend', 1917 | 'OTransition' : 'oTransitionEnd otransitionend', 1918 | 'transition' : 'transitionend' 1919 | } 1920 | 1921 | for (var name in transEndEventNames) { 1922 | if (el.style[name] !== undefined) { 1923 | return { end: transEndEventNames[name] } 1924 | } 1925 | } 1926 | 1927 | return false // explicit for ie8 ( ._.) 1928 | } 1929 | 1930 | // http://blog.alexmaccaw.com/css-transitions 1931 | $.fn.emulateTransitionEnd = function (duration) { 1932 | var called = false, $el = this 1933 | $(this).one($.support.transition.end, function () { called = true }) 1934 | var callback = function () { if (!called) $($el).trigger($.support.transition.end) } 1935 | setTimeout(callback, duration) 1936 | return this 1937 | } 1938 | 1939 | $(function () { 1940 | $.support.transition = transitionEnd() 1941 | }) 1942 | 1943 | }(jQuery); 1944 | --------------------------------------------------------------------------------
This is an example sentence with eight words.