diff --git a/Gemfile b/Gemfile
index ae1107d6..e99e5dc2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -60,7 +60,11 @@ end
group :test do
gem 'pry'
- gem 'steak'
+ #gem 'steak'
+ gem 'rspec-rails'
+ gem 'cucumber-rails'
+ gem 'poltergeist'
+ gem 'database_cleaner'
gem 'rb-fsevent', :require => false #if RUBY_PLATFORM =~ /darwin/i
gem 'ruby_gntp'
gem 'guard-rspec'
diff --git a/Gemfile.lock b/Gemfile.lock
index 8e69f3e7..b39843ea 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -106,7 +106,17 @@ GEM
mime-types (~> 1.15)
multi_json (~> 1.0)
rest-client (~> 1.6.1)
+ cucumber (1.2.3)
+ builder (>= 2.1.2)
+ diff-lcs (>= 1.1.3)
+ gherkin (~> 2.11.6)
+ multi_json (~> 1.3)
+ cucumber-rails (1.3.0)
+ capybara (>= 1.1.2)
+ cucumber (>= 1.1.8)
+ nokogiri (>= 1.5.0)
daemons (1.1.9)
+ database_cleaner (0.9.1)
devise (2.0.4)
bcrypt-ruby (~> 3.0)
orm_adapter (~> 0.0.3)
@@ -126,8 +136,12 @@ GEM
factory_girl_rails (4.2.1)
factory_girl (~> 4.2.0)
railties (>= 3.0.0)
+ faye-websocket (0.4.6)
+ eventmachine (>= 0.12.0)
ffi (1.4.0)
fssm (0.2.10)
+ gherkin (2.11.6)
+ json (>= 1.7.6)
guard (1.6.2)
listen (>= 0.6.0)
lumberjack (>= 1.0.2)
@@ -145,6 +159,7 @@ GEM
haml (>= 3.1, < 4.1)
railties (>= 3.1, < 4.1)
hike (1.2.1)
+ http_parser.rb (0.5.3)
i18n (0.6.4)
journey (1.0.4)
jquery-rails (2.2.1)
@@ -175,6 +190,12 @@ GEM
activesupport (>= 3.0.0)
cocaine (~> 0.5.0)
mime-types
+ poltergeist (1.0.2)
+ capybara (> 1.1)
+ childprocess (~> 0.3)
+ faye-websocket (~> 0.4, >= 0.4.4)
+ http_parser.rb (~> 0.5.3)
+ multi_json (~> 1.0)
polyglot (0.3.3)
pry (0.9.12)
coderay (~> 1.0.5)
@@ -257,9 +278,6 @@ GEM
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
- steak (2.0.0)
- capybara (>= 1.0.0)
- rspec-rails (>= 2.5.0)
subexec (0.2.2)
temple (0.5.5)
terminal-table (1.4.5)
@@ -295,6 +313,8 @@ DEPENDENCIES
coffee-rails (~> 3.2.1)
compass-rails
couch_potato!
+ cucumber-rails
+ database_cleaner
devise (= 2.0.4)
devise_simply_stored
factory_girl_rails
@@ -304,6 +324,7 @@ DEPENDENCIES
letter_opener
mini_magick
mustache
+ poltergeist
pry
quiet_assets
rack-cors
@@ -316,6 +337,5 @@ DEPENDENCIES
simple_form
simply_stored!
slim-rails
- steak
thin
uglifier (>= 1.0.3)
diff --git a/app/assets/javascripts/supplier/application.js b/app/assets/javascripts/supplier/application.js
index 84470745..085a322a 100644
--- a/app/assets/javascripts/supplier/application.js
+++ b/app/assets/javascripts/supplier/application.js
@@ -17,10 +17,11 @@
// require bootstrap-popover
// require bootstrap-typeahead
//= require bootstrap
-//= require mustache
+//= require handlebars
//= require faye
//= require supplier/base
//= require qwaiter
+//= require qtip
//= require_directory .
//= require_self
//= require moment
@@ -45,3 +46,16 @@ function redirect_to(mapping, variables){
function currency(num) {
return Qwaiter.currency(num);
}
+
+Handlebars.registerHelper('t', function(tlocation) {
+ return t(tlocation)
+})
+Handlebars.registerHelper('currency', function(price) {
+ if(price.fn){
+ price = price.fn(this);
+ }
+ if(typeof(price) == 'function'){
+ price = price.call(this)
+ }
+ return new Handlebars.SafeString(currency(price))
+})
diff --git a/app/assets/javascripts/supplier/qsupplier.js.coffee b/app/assets/javascripts/supplier/qsupplier.js.coffee
index bfde9708..c0ea7b65 100644
--- a/app/assets/javascripts/supplier/qsupplier.js.coffee
+++ b/app/assets/javascripts/supplier/qsupplier.js.coffee
@@ -6,7 +6,8 @@ root.Qsupplier=
if(e.event == 'new_order')
body = $('#active-orders-table tbody')
order = new Order(e.data)
- body.append @mustache('#active-order-template', order)
+ if body.length
+ body.append @mustache('#active-order-template', order)
$('.section-table-list-'+order.list_id()).addClass('active_order')
else if(e.event == 'list_needs_help')
$('#list-needs-help-indicator-'+e.data.id).removeClass('hide')
@@ -202,6 +203,11 @@ root.Qsupplier=
mustache: (selector, locals)->
html = $(selector).html()
return '' unless html
+ template = Handlebars.compile(html)
+ container = $('
')
+ container.html(template(locals))
+ setTranslations(container)
+ return container.html()
locs = $.extend(locals,
currency: ->
(val)->
diff --git a/app/assets/javascripts/supplier/translations.js.erb b/app/assets/javascripts/supplier/translations.js.erb
index 1e755beb..67a5407b 100644
--- a/app/assets/javascripts/supplier/translations.js.erb
+++ b/app/assets/javascripts/supplier/translations.js.erb
@@ -61,7 +61,7 @@ function setTranslations(selector){
list.find('.locale').show();
list.find('.locale-'+$locale).hide();
if(selector){
- $(selector).find('[data-t]').each(function(){$(this).text(t($(this).data('t'), $(this).data('tAttributes')))})
+ $(selector).find('[data-t]').each(function(){$(this).html(t($(this).data('t'), $(this).data('tAttributes')))})
}else{
$('[data-t]').each(function(){$(this).html(t($(this).data('t'),$(this).data('tAttributes')))})
}
diff --git a/app/assets/stylesheets/supplier/application.css b/app/assets/stylesheets/supplier/application.css
index 15383502..ba917973 100644
--- a/app/assets/stylesheets/supplier/application.css
+++ b/app/assets/stylesheets/supplier/application.css
@@ -2,6 +2,7 @@
*= require 'twitter-bootstrap/bootstrap_and_overrides'
*= require 'twitter-bootstrap/bootstrap_overrides'
*= require 'jquery-ui-1.8.23.custom.css'
+ *= require qtip
*= require 'general'
*= require user/active_list
*= require_directory .
diff --git a/app/assets/stylesheets/supplier/darkstrap.sass b/app/assets/stylesheets/supplier/darkstrap.sass
index eaef076d..5be04f3f 100644
--- a/app/assets/stylesheets/supplier/darkstrap.sass
+++ b/app/assets/stylesheets/supplier/darkstrap.sass
@@ -265,6 +265,11 @@ legend
border-top: 1px solid #222
+box-shadow(0 1px 0 #333333 inset)
+.popover
+ color: #333
+ h3
+ color: #333
+
//=Progress bars
.progress
@extend .well
diff --git a/app/assets/stylesheets/supplier/section_tables.css.sass b/app/assets/stylesheets/supplier/section_tables.css.sass
index aade0451..cf7ca25f 100644
--- a/app/assets/stylesheets/supplier/section_tables.css.sass
+++ b/app/assets/stylesheets/supplier/section_tables.css.sass
@@ -40,7 +40,7 @@ $table-width: 83px
background-color: rgba(0,0,0,0.4)
.section-table
position: absolute
- cursor: move
+ cursor: pointer
&.occupied
background-color: #ffa
&.needs_help
@@ -59,6 +59,9 @@ $table-width: 83px
a
&:hover
text-decoration: none
+ &.section-tables-manage
+ .section-table
+ cursor: move
&.section-tables-inactive
.section-table
position: relative
@@ -76,3 +79,7 @@ table
content: " - "
.go-to-tables-view.hide
display: inline-block
+
+ul#table-actions-list
+ list-style: none
+ margin: 0
diff --git a/app/controllers/suppliers/sections_controller.rb b/app/controllers/suppliers/sections_controller.rb
index ed702e3b..e04072c7 100644
--- a/app/controllers/suppliers/sections_controller.rb
+++ b/app/controllers/suppliers/sections_controller.rb
@@ -103,6 +103,7 @@ module Suppliers
# GET /sections/1/tables_view.json
def tables_view
@section = Section.find_by_supplier_id_and_id!(current_supplier.id, params[:id])
+ @tables = Table.enrich_active_list_id(@section.tables)
respond_to do |format|
format.html # show.html.erb
@@ -138,5 +139,15 @@ module Suppliers
else render(json: json_alert('messages.could_not_arrange_tables'))
end
end
+
+ def table_actions
+ @section = Section.find_by_supplier_id_and_id!(current_supplier.id, params[:id])
+ render(text: 'No table_id given', content_type: Mime::HTML.to_s) and return unless params[:table_id].present?
+ @table = Table.find(params[:table_id])
+ render(text: 'Table is not correct', content_type: Mime::HTML.to_s) and return unless @table.section_id == @section.id
+ @list = List.active_for_table(@table).first
+ @orders = @list ? @list.active_orders : []
+ render layout: false
+ end
end
end
diff --git a/app/models/list.rb b/app/models/list.rb
index 686bce99..fca9e80d 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -314,6 +314,10 @@ class List
Order.count_active_for_supplier_and_list(supplier_id, id) > 0
end
+ def active_orders
+ Order.active_for_supplier_and_list(supplier_id, id)
+ end
+
# Return a join requests object in the form of:
# {join_request: [{user_id: '1saf3...', user_email: 'info@qwaiter.com'}, [....]]}
def join_requests_as_json
diff --git a/app/templates/supplier/_active_list.mustache b/app/templates/supplier/_active_list.mustache
index e528a454..d29c5560 100644
--- a/app/templates/supplier/_active_list.mustache
+++ b/app/templates/supplier/_active_list.mustache
@@ -5,10 +5,10 @@
{{table_number}} |
{{section_title}} |
- {{#currency}}{{total_amount}}{{/currency}} |
+ {{currency total_amount}} |
-
-
+
+
|
diff --git a/app/templates/supplier/_active_order.mustache b/app/templates/supplier/_active_order.mustache
index e579af92..5a69092b 100644
--- a/app/templates/supplier/_active_order.mustache
+++ b/app/templates/supplier/_active_order.mustache
@@ -2,9 +2,9 @@
| {{display}} |
{{table_number}} |
{{section_title}} |
- {{#currency}}{{total_amount}}{{/currency}} |
+ {{currency total_amount}} |
-
-
+
+
|
diff --git a/app/views/suppliers/sections/manage_tables.html.slim b/app/views/suppliers/sections/manage_tables.html.slim
index a31b965b..3725fc36 100644
--- a/app/views/suppliers/sections/manage_tables.html.slim
+++ b/app/views/suppliers/sections/manage_tables.html.slim
@@ -6,7 +6,7 @@
- for section in @section.supplier.sections
li class=(section == @section ? 'active' : nil) = link_to section.title, [:manage_tables, :suppliers, section]
.span9
- .well.section-tables-container.section-tables-active
+ .well.section-tables-container.section-tables-active.section-tables-manage
- for table in @section.tables
.section-table.hide{ id="section-table-#{table.id}" data-position-x=table.position_x data-position-y=table.position_y data-table-id=table.id}
.pull-right.action-button-container
diff --git a/app/views/suppliers/sections/table_actions.html.slim b/app/views/suppliers/sections/table_actions.html.slim
new file mode 100644
index 00000000..9d8c4489
--- /dev/null
+++ b/app/views/suppliers/sections/table_actions.html.slim
@@ -0,0 +1,8 @@
+ul#table-actions-list
+ - if @list
+ - if @list.needs_help?
+ li
+ button.btn.btn-info.btn-small class="of-list-#{@list.id}" id="list-is-helped-button-#{@list.id}" onclick="Qsupplier.mark_list_as_helped('#{@list.id}')" data-t="list.is_helped_button"
+ button.btn.btn-warning.btn-small class="of-list-#{@list.id}" onclick="Qsupplier.close_list('#{@list.id}')" data-t="list.close_list"
+ li
+ a data-t='section.tables_view.table_actions.got_to_table' href=suppliers_table_path(@table)
diff --git a/app/views/suppliers/sections/tables_view.html.slim b/app/views/suppliers/sections/tables_view.html.slim
index 7bc89573..ce81c07e 100644
--- a/app/views/suppliers/sections/tables_view.html.slim
+++ b/app/views/suppliers/sections/tables_view.html.slim
@@ -4,15 +4,21 @@
.span12
.well.section-tables-container.section-tables-active
.section-manage-tables.pull-right= link_to content_tag(:span, '', class: 'icon-pencil'), manage_tables_suppliers_section_path(@section), class: 'btn btn-mini'
- - for table in Table.enrich_active_list_id(@section.tables)
+ - for table in @tables
.section-table.hide[
class="section-table-list-#{table.active_list_id}" id="section-table-#{table.id}"
data-position-x=table.position_x data-position-y=table.position_y data-table-id=table.id]
- .table-number = link_to table.number, suppliers_table_path(table)
+ .table-number = table.number
.status-icons
span.needs_payment.icon-flag
span.needs_help.icon-bell
span.active_order.icon-glass
+#section-table-menu-container.hide
+ - @tables.each do |table|
+ .section-table-menu-content class="section-table-menu-#{table.id} section-table-list-#{table.active_list_id}"
+ button.btn.btn-info.list-is-helped.hide Question answered!
+
+
- content_for :footer do
javascript:
var current_section_id = '#{@section.id}';
@@ -24,6 +30,38 @@
active_section_container.css('height', #{@section.height/@section.width}*active_section_container.width());
active_section_container.find('.section-table').each(function(){
Qsupplier.position_table_in_active_section(active_section_container, $(this), false);
+ var table_id;
+ var match = $(this).attr('id').match(/section-table-(\w+)/);
+ if(!match || match.length < 2) return;
+ table_id = match[1];
+
+ $(this).qtip({
+ content: {
+ text: '#{spinner}',
+ ajax: {
+ url: '#{table_actions_suppliers_section_path(@section)}',
+ data: {table_id: table_id},
+ success: function(data, status){
+ var container = $('');
+ container.html(data); // Create a container to parse translation data
+ setTranslations(container);
+ this.set('content.text', container.html());
+ },
+ once: false
+ },
+ title: {
+ text: t('section.tables_view.table_actions.title'),
+ button: true
+ }
+ },
+ show: {
+ event: 'click'
+ },
+ hide: 'unfocus',
+ style: {
+ classes: 'qtip-wiki qtip-light qtip-shadow'
+ }
+ })
});
Qsupplier.update_section_tables_view('#{@section.id}');
Qsupplier.watch_events();
diff --git a/config/cucumber.yml b/config/cucumber.yml
new file mode 100644
index 00000000..19b288df
--- /dev/null
+++ b/config/cucumber.yml
@@ -0,0 +1,8 @@
+<%
+rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
+rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}"
+std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip"
+%>
+default: <%= std_opts %> features
+wip: --tags @wip:3 --wip features
+rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip
diff --git a/config/database.yml b/config/database.yml
index 51a4dd45..09cc85a2 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -12,7 +12,7 @@ development:
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
-test:
+test: &test
adapter: sqlite3
database: db/test.sqlite3
pool: 5
@@ -23,3 +23,6 @@ production:
database: db/production.sqlite3
pool: 5
timeout: 5000
+
+cucumber:
+ <<: *test
\ No newline at end of file
diff --git a/config/locales/supplier.en.yml b/config/locales/supplier.en.yml
index 39513bbf..b692f4c2 100644
--- a/config/locales/supplier.en.yml
+++ b/config/locales/supplier.en.yml
@@ -26,6 +26,12 @@ en:
show_all: Show all ${models.plural.list}
show:
title: Show %{list}
+ list:
+ is_helped_button: Question answered!
+ close_list: Close!
+ order:
+ being_processed: 'In process!'
+ being_served: 'Is delivered!'
section:
first_section_title: Room
show:
@@ -35,6 +41,9 @@ en:
title: "Manage tables for ${models.section|downcase}: %{title}"
tables_view:
link: 'Tables view'
+ table_actions:
+ title: '${models.table} actions'
+ got_to_table: 'Go to ${models.table|downcase}'
add_tables:
button_label: Add tables
modal:
diff --git a/config/locales/supplier.nl.yml b/config/locales/supplier.nl.yml
index 249f2ef5..43ec1413 100644
--- a/config/locales/supplier.nl.yml
+++ b/config/locales/supplier.nl.yml
@@ -26,6 +26,12 @@ nl:
show_all: Toon alle ${models.plural.list}
show:
title: "%{list} tonen"
+ list:
+ is_helped_button: Vraag beantwoord!
+ close_list: Afsluiten!
+ order:
+ being_processed: 'Ben bezig!'
+ being_served: 'Ik kom het brengen!'
section:
first_section_title: Ruimte
show:
@@ -35,6 +41,9 @@ nl:
title: "Tafels beheren voor ${models.section|downcase}: %{title}"
tables_view:
link: Tafel overzicht
+ table_actions:
+ title: '${models.table} acties'
+ got_to_table: 'Toon ${models.table|downcase}'
add_tables:
button_label: Voeg tafels toe
modal:
diff --git a/config/routes.rb b/config/routes.rb
index 05f2997d..fb611ac1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -78,6 +78,7 @@ Qwaiter::Application.routes.draw do
member do
get :manage_tables
get :tables_view
+ get :table_actions
post :add_tables
post :arrange_tables
end
diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake
new file mode 100644
index 00000000..83f79471
--- /dev/null
+++ b/lib/tasks/cucumber.rake
@@ -0,0 +1,65 @@
+# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
+# It is recommended to regenerate this file in the future when you upgrade to a
+# newer version of cucumber-rails. Consider adding your own code to a new file
+# instead of editing this one. Cucumber will automatically load all features/**/*.rb
+# files.
+
+
+unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks
+
+vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
+$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil?
+
+begin
+ require 'cucumber/rake/task'
+
+ namespace :cucumber do
+ Cucumber::Rake::Task.new({:ok => 'db:test:prepare'}, 'Run features that should pass') do |t|
+ t.binary = vendored_cucumber_bin # If nil, the gem's binary is used.
+ t.fork = true # You may get faster startup if you set this to false
+ t.profile = 'default'
+ end
+
+ Cucumber::Rake::Task.new({:wip => 'db:test:prepare'}, 'Run features that are being worked on') do |t|
+ t.binary = vendored_cucumber_bin
+ t.fork = true # You may get faster startup if you set this to false
+ t.profile = 'wip'
+ end
+
+ Cucumber::Rake::Task.new({:rerun => 'db:test:prepare'}, 'Record failing features and run only them if any exist') do |t|
+ t.binary = vendored_cucumber_bin
+ t.fork = true # You may get faster startup if you set this to false
+ t.profile = 'rerun'
+ end
+
+ desc 'Run all features'
+ task :all => [:ok, :wip]
+
+ task :statsetup do
+ require 'rails/code_statistics'
+ ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features')
+ ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features')
+ end
+ end
+ desc 'Alias for cucumber:ok'
+ task :cucumber => 'cucumber:ok'
+
+ task :default => :cucumber
+
+ task :features => :cucumber do
+ STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***"
+ end
+
+ # In case we don't have ActiveRecord, append a no-op task that we can depend upon.
+ task 'db:test:prepare' do
+ end
+
+ task :stats => 'cucumber:statsetup'
+rescue LoadError
+ desc 'cucumber rake task not available (cucumber not installed)'
+ task :cucumber do
+ abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin'
+ end
+end
+
+end
diff --git a/script/cucumber b/script/cucumber
new file mode 100755
index 00000000..7fa5c920
--- /dev/null
+++ b/script/cucumber
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+
+vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
+if vendored_cucumber_bin
+ load File.expand_path(vendored_cucumber_bin)
+else
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
+ require 'cucumber'
+ load Cucumber::BINARY
+end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index da401cc3..6062f953 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -83,4 +83,32 @@ describe List do
end
end
+ describe '#active_orders' do
+ its(:active_orders) { should be_empty }
+
+ it 'returns placed orders' do
+ order = create :order, supplier: supplier, list: list, section: section, state: 'placed'
+ list.active_orders.should eq [order]
+ end
+
+ it 'returns active orders' do
+ order = create :order, supplier: supplier, list: list, section: section, state: 'active'
+ list.active_orders.should eq [order]
+ end
+
+ it 'does not return delivered orders' do
+ order = create :order, supplier: supplier, list: list, section: section, state: 'delivered'
+ list.active_orders.should be_empty
+ end
+
+ it 'does not return closed orders' do
+ order = create :order, supplier: supplier, list: list, section: section, state: 'closed'
+ list.active_orders.should be_empty
+ end
+ it 'does not return cancelled orders' do
+ order = create :order, supplier: supplier, list: list, section: section, state: 'cancelled'
+ list.active_orders.should be_empty
+ end
+ end
+
end
diff --git a/vendor/assets/javascripts/handlebars.js b/vendor/assets/javascripts/handlebars.js
new file mode 100644
index 00000000..9c653ee7
--- /dev/null
+++ b/vendor/assets/javascripts/handlebars.js
@@ -0,0 +1,2201 @@
+/*
+
+Copyright (C) 2011 by Yehuda Katz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+// lib/handlebars/base.js
+
+/*jshint eqnull:true*/
+this.Handlebars = {};
+
+(function(Handlebars) {
+
+Handlebars.VERSION = "1.0.0-rc.3";
+Handlebars.COMPILER_REVISION = 2;
+
+Handlebars.REVISION_CHANGES = {
+ 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
+ 2: '>= 1.0.0-rc.3'
+};
+
+Handlebars.helpers = {};
+Handlebars.partials = {};
+
+Handlebars.registerHelper = function(name, fn, inverse) {
+ if(inverse) { fn.not = inverse; }
+ this.helpers[name] = fn;
+};
+
+Handlebars.registerPartial = function(name, str) {
+ this.partials[name] = str;
+};
+
+Handlebars.registerHelper('helperMissing', function(arg) {
+ if(arguments.length === 2) {
+ return undefined;
+ } else {
+ throw new Error("Could not find property '" + arg + "'");
+ }
+});
+
+var toString = Object.prototype.toString, functionType = "[object Function]";
+
+Handlebars.registerHelper('blockHelperMissing', function(context, options) {
+ var inverse = options.inverse || function() {}, fn = options.fn;
+
+
+ var ret = "";
+ var type = toString.call(context);
+
+ if(type === functionType) { context = context.call(this); }
+
+ if(context === true) {
+ return fn(this);
+ } else if(context === false || context == null) {
+ return inverse(this);
+ } else if(type === "[object Array]") {
+ if(context.length > 0) {
+ return Handlebars.helpers.each(context, options);
+ } else {
+ return inverse(this);
+ }
+ } else {
+ return fn(context);
+ }
+});
+
+Handlebars.K = function() {};
+
+Handlebars.createFrame = Object.create || function(object) {
+ Handlebars.K.prototype = object;
+ var obj = new Handlebars.K();
+ Handlebars.K.prototype = null;
+ return obj;
+};
+
+Handlebars.logger = {
+ DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
+
+ methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'},
+
+ // can be overridden in the host environment
+ log: function(level, obj) {
+ if (Handlebars.logger.level <= level) {
+ var method = Handlebars.logger.methodMap[level];
+ if (typeof console !== 'undefined' && console[method]) {
+ console[method].call(console, obj);
+ }
+ }
+ }
+};
+
+Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); };
+
+Handlebars.registerHelper('each', function(context, options) {
+ var fn = options.fn, inverse = options.inverse;
+ var i = 0, ret = "", data;
+
+ if (options.data) {
+ data = Handlebars.createFrame(options.data);
+ }
+
+ if(context && typeof context === 'object') {
+ if(context instanceof Array){
+ for(var j = context.length; i 2) {
+ expected.push("'" + this.terminals_[p] + "'");
+ }
+ if (this.lexer.showPosition) {
+ errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'";
+ } else {
+ errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'");
+ }
+ this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected});
+ }
+ }
+ if (action[0] instanceof Array && action.length > 1) {
+ throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol);
+ }
+ switch (action[0]) {
+ case 1:
+ stack.push(symbol);
+ vstack.push(this.lexer.yytext);
+ lstack.push(this.lexer.yylloc);
+ stack.push(action[1]);
+ symbol = null;
+ if (!preErrorSymbol) {
+ yyleng = this.lexer.yyleng;
+ yytext = this.lexer.yytext;
+ yylineno = this.lexer.yylineno;
+ yyloc = this.lexer.yylloc;
+ if (recovering > 0)
+ recovering--;
+ } else {
+ symbol = preErrorSymbol;
+ preErrorSymbol = null;
+ }
+ break;
+ case 2:
+ len = this.productions_[action[1]][1];
+ yyval.$ = vstack[vstack.length - len];
+ yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column};
+ if (ranges) {
+ yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]];
+ }
+ r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
+ if (typeof r !== "undefined") {
+ return r;
+ }
+ if (len) {
+ stack = stack.slice(0, -1 * len * 2);
+ vstack = vstack.slice(0, -1 * len);
+ lstack = lstack.slice(0, -1 * len);
+ }
+ stack.push(this.productions_[action[1]][0]);
+ vstack.push(yyval.$);
+ lstack.push(yyval._$);
+ newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
+ stack.push(newState);
+ break;
+ case 3:
+ return true;
+ }
+ }
+ return true;
+}
+};
+/* Jison generated lexer */
+var lexer = (function(){
+var lexer = ({EOF:1,
+parseError:function parseError(str, hash) {
+ if (this.yy.parser) {
+ this.yy.parser.parseError(str, hash);
+ } else {
+ throw new Error(str);
+ }
+ },
+setInput:function (input) {
+ this._input = input;
+ this._more = this._less = this.done = false;
+ this.yylineno = this.yyleng = 0;
+ this.yytext = this.matched = this.match = '';
+ this.conditionStack = ['INITIAL'];
+ this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0};
+ if (this.options.ranges) this.yylloc.range = [0,0];
+ this.offset = 0;
+ return this;
+ },
+input:function () {
+ var ch = this._input[0];
+ this.yytext += ch;
+ this.yyleng++;
+ this.offset++;
+ this.match += ch;
+ this.matched += ch;
+ var lines = ch.match(/(?:\r\n?|\n).*/g);
+ if (lines) {
+ this.yylineno++;
+ this.yylloc.last_line++;
+ } else {
+ this.yylloc.last_column++;
+ }
+ if (this.options.ranges) this.yylloc.range[1]++;
+
+ this._input = this._input.slice(1);
+ return ch;
+ },
+unput:function (ch) {
+ var len = ch.length;
+ var lines = ch.split(/(?:\r\n?|\n)/g);
+
+ this._input = ch + this._input;
+ this.yytext = this.yytext.substr(0, this.yytext.length-len-1);
+ //this.yyleng -= len;
+ this.offset -= len;
+ var oldLines = this.match.split(/(?:\r\n?|\n)/g);
+ this.match = this.match.substr(0, this.match.length-1);
+ this.matched = this.matched.substr(0, this.matched.length-1);
+
+ if (lines.length-1) this.yylineno -= lines.length-1;
+ var r = this.yylloc.range;
+
+ this.yylloc = {first_line: this.yylloc.first_line,
+ last_line: this.yylineno+1,
+ first_column: this.yylloc.first_column,
+ last_column: lines ?
+ (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length:
+ this.yylloc.first_column - len
+ };
+
+ if (this.options.ranges) {
+ this.yylloc.range = [r[0], r[0] + this.yyleng - len];
+ }
+ return this;
+ },
+more:function () {
+ this._more = true;
+ return this;
+ },
+less:function (n) {
+ this.unput(this.match.slice(n));
+ },
+pastInput:function () {
+ var past = this.matched.substr(0, this.matched.length - this.match.length);
+ return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
+ },
+upcomingInput:function () {
+ var next = this.match;
+ if (next.length < 20) {
+ next += this._input.substr(0, 20-next.length);
+ }
+ return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, "");
+ },
+showPosition:function () {
+ var pre = this.pastInput();
+ var c = new Array(pre.length + 1).join("-");
+ return pre + this.upcomingInput() + "\n" + c+"^";
+ },
+next:function () {
+ if (this.done) {
+ return this.EOF;
+ }
+ if (!this._input) this.done = true;
+
+ var token,
+ match,
+ tempMatch,
+ index,
+ col,
+ lines;
+ if (!this._more) {
+ this.yytext = '';
+ this.match = '';
+ }
+ var rules = this._currentRules();
+ for (var i=0;i < rules.length; i++) {
+ tempMatch = this._input.match(this.rules[rules[i]]);
+ if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
+ match = tempMatch;
+ index = i;
+ if (!this.options.flex) break;
+ }
+ }
+ if (match) {
+ lines = match[0].match(/(?:\r\n?|\n).*/g);
+ if (lines) this.yylineno += lines.length;
+ this.yylloc = {first_line: this.yylloc.last_line,
+ last_line: this.yylineno+1,
+ first_column: this.yylloc.last_column,
+ last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length};
+ this.yytext += match[0];
+ this.match += match[0];
+ this.matches = match;
+ this.yyleng = this.yytext.length;
+ if (this.options.ranges) {
+ this.yylloc.range = [this.offset, this.offset += this.yyleng];
+ }
+ this._more = false;
+ this._input = this._input.slice(match[0].length);
+ this.matched += match[0];
+ token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]);
+ if (this.done && this._input) this.done = false;
+ if (token) return token;
+ else return;
+ }
+ if (this._input === "") {
+ return this.EOF;
+ } else {
+ return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(),
+ {text: "", token: null, line: this.yylineno});
+ }
+ },
+lex:function lex() {
+ var r = this.next();
+ if (typeof r !== 'undefined') {
+ return r;
+ } else {
+ return this.lex();
+ }
+ },
+begin:function begin(condition) {
+ this.conditionStack.push(condition);
+ },
+popState:function popState() {
+ return this.conditionStack.pop();
+ },
+_currentRules:function _currentRules() {
+ return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules;
+ },
+topState:function () {
+ return this.conditionStack[this.conditionStack.length-2];
+ },
+pushState:function begin(condition) {
+ this.begin(condition);
+ }});
+lexer.options = {};
+lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
+
+var YYSTATE=YY_START
+switch($avoiding_name_collisions) {
+case 0:
+ if(yy_.yytext.slice(-1) !== "\\") this.begin("mu");
+ if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu");
+ if(yy_.yytext) return 14;
+
+break;
+case 1: return 14;
+break;
+case 2:
+ if(yy_.yytext.slice(-1) !== "\\") this.popState();
+ if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1);
+ return 14;
+
+break;
+case 3: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15;
+break;
+case 4: this.begin("par"); return 24;
+break;
+case 5: return 16;
+break;
+case 6: return 20;
+break;
+case 7: return 19;
+break;
+case 8: return 19;
+break;
+case 9: return 23;
+break;
+case 10: return 23;
+break;
+case 11: this.popState(); this.begin('com');
+break;
+case 12: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15;
+break;
+case 13: return 22;
+break;
+case 14: return 36;
+break;
+case 15: return 35;
+break;
+case 16: return 35;
+break;
+case 17: return 39;
+break;
+case 18: /*ignore whitespace*/
+break;
+case 19: this.popState(); return 18;
+break;
+case 20: this.popState(); return 18;
+break;
+case 21: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 30;
+break;
+case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 30;
+break;
+case 23: yy_.yytext = yy_.yytext.substr(1); return 28;
+break;
+case 24: return 32;
+break;
+case 25: return 32;
+break;
+case 26: return 31;
+break;
+case 27: return 35;
+break;
+case 28: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 35;
+break;
+case 29: return 'INVALID';
+break;
+case 30: /*ignore whitespace*/
+break;
+case 31: this.popState(); return 37;
+break;
+case 32: return 5;
+break;
+}
+};
+lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[} ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:\s+)/,/^(?:[a-zA-Z0-9_$-/]+)/,/^(?:$)/];
+lexer.conditions = {"mu":{"rules":[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,32],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[3],"inclusive":false},"par":{"rules":[30,31],"inclusive":false},"INITIAL":{"rules":[0,1,32],"inclusive":true}};
+return lexer;})()
+parser.lexer = lexer;
+function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser;
+return new Parser;
+})();;
+// lib/handlebars/compiler/base.js
+Handlebars.Parser = handlebars;
+
+Handlebars.parse = function(input) {
+
+ // Just return if an already-compile AST was passed in.
+ if(input.constructor === Handlebars.AST.ProgramNode) { return input; }
+
+ Handlebars.Parser.yy = Handlebars.AST;
+ return Handlebars.Parser.parse(input);
+};
+
+Handlebars.print = function(ast) {
+ return new Handlebars.PrintVisitor().accept(ast);
+};;
+// lib/handlebars/compiler/ast.js
+(function() {
+
+ Handlebars.AST = {};
+
+ Handlebars.AST.ProgramNode = function(statements, inverse) {
+ this.type = "program";
+ this.statements = statements;
+ if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); }
+ };
+
+ Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) {
+ this.type = "mustache";
+ this.escaped = !unescaped;
+ this.hash = hash;
+
+ var id = this.id = rawParams[0];
+ var params = this.params = rawParams.slice(1);
+
+ // a mustache is an eligible helper if:
+ // * its id is simple (a single part, not `this` or `..`)
+ var eligibleHelper = this.eligibleHelper = id.isSimple;
+
+ // a mustache is definitely a helper if:
+ // * it is an eligible helper, and
+ // * it has at least one parameter or hash segment
+ this.isHelper = eligibleHelper && (params.length || hash);
+
+ // if a mustache is an eligible helper but not a definite
+ // helper, it is ambiguous, and will be resolved in a later
+ // pass or at runtime.
+ };
+
+ Handlebars.AST.PartialNode = function(partialName, context) {
+ this.type = "partial";
+ this.partialName = partialName;
+ this.context = context;
+ };
+
+ var verifyMatch = function(open, close) {
+ if(open.original !== close.original) {
+ throw new Handlebars.Exception(open.original + " doesn't match " + close.original);
+ }
+ };
+
+ Handlebars.AST.BlockNode = function(mustache, program, inverse, close) {
+ verifyMatch(mustache.id, close);
+ this.type = "block";
+ this.mustache = mustache;
+ this.program = program;
+ this.inverse = inverse;
+
+ if (this.inverse && !this.program) {
+ this.isInverse = true;
+ }
+ };
+
+ Handlebars.AST.ContentNode = function(string) {
+ this.type = "content";
+ this.string = string;
+ };
+
+ Handlebars.AST.HashNode = function(pairs) {
+ this.type = "hash";
+ this.pairs = pairs;
+ };
+
+ Handlebars.AST.IdNode = function(parts) {
+ this.type = "ID";
+ this.original = parts.join(".");
+
+ var dig = [], depth = 0;
+
+ for(var i=0,l=parts.length; i 0) { throw new Handlebars.Exception("Invalid path: " + this.original); }
+ else if (part === "..") { depth++; }
+ else { this.isScoped = true; }
+ }
+ else { dig.push(part); }
+ }
+
+ this.parts = dig;
+ this.string = dig.join('.');
+ this.depth = depth;
+
+ // an ID is simple if it only has one part, and that part is not
+ // `..` or `this`.
+ this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
+
+ this.stringModeValue = this.string;
+ };
+
+ Handlebars.AST.PartialNameNode = function(name) {
+ this.type = "PARTIAL_NAME";
+ this.name = name;
+ };
+
+ Handlebars.AST.DataNode = function(id) {
+ this.type = "DATA";
+ this.id = id;
+ };
+
+ Handlebars.AST.StringNode = function(string) {
+ this.type = "STRING";
+ this.string = string;
+ this.stringModeValue = string;
+ };
+
+ Handlebars.AST.IntegerNode = function(integer) {
+ this.type = "INTEGER";
+ this.integer = integer;
+ this.stringModeValue = Number(integer);
+ };
+
+ Handlebars.AST.BooleanNode = function(bool) {
+ this.type = "BOOLEAN";
+ this.bool = bool;
+ this.stringModeValue = bool === "true";
+ };
+
+ Handlebars.AST.CommentNode = function(comment) {
+ this.type = "comment";
+ this.comment = comment;
+ };
+
+})();;
+// lib/handlebars/utils.js
+
+var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
+
+Handlebars.Exception = function(message) {
+ var tmp = Error.prototype.constructor.apply(this, arguments);
+
+ // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
+ for (var idx = 0; idx < errorProps.length; idx++) {
+ this[errorProps[idx]] = tmp[errorProps[idx]];
+ }
+};
+Handlebars.Exception.prototype = new Error();
+
+// Build out our basic SafeString type
+Handlebars.SafeString = function(string) {
+ this.string = string;
+};
+Handlebars.SafeString.prototype.toString = function() {
+ return this.string.toString();
+};
+
+(function() {
+ var escape = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
+ "`": "`"
+ };
+
+ var badChars = /[&<>"'`]/g;
+ var possible = /[&<>"'`]/;
+
+ var escapeChar = function(chr) {
+ return escape[chr] || "&";
+ };
+
+ Handlebars.Utils = {
+ escapeExpression: function(string) {
+ // don't escape SafeStrings, since they're already safe
+ if (string instanceof Handlebars.SafeString) {
+ return string.toString();
+ } else if (string == null || string === false) {
+ return "";
+ }
+
+ if(!possible.test(string)) { return string; }
+ return string.replace(badChars, escapeChar);
+ },
+
+ isEmpty: function(value) {
+ if (!value && value !== 0) {
+ return true;
+ } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+})();;
+// lib/handlebars/compiler/compiler.js
+
+/*jshint eqnull:true*/
+Handlebars.Compiler = function() {};
+Handlebars.JavaScriptCompiler = function() {};
+
+(function(Compiler, JavaScriptCompiler) {
+ // the foundHelper register will disambiguate helper lookup from finding a
+ // function in a context. This is necessary for mustache compatibility, which
+ // requires that context functions in blocks are evaluated by blockHelperMissing,
+ // and then proceed as if the resulting value was provided to blockHelperMissing.
+
+ Compiler.prototype = {
+ compiler: Compiler,
+
+ disassemble: function() {
+ var opcodes = this.opcodes, opcode, out = [], params, param;
+
+ for (var i=0, l=opcodes.length; i 0) {
+ this.source[1] = this.source[1] + ", " + locals.join(", ");
+ }
+
+ // Generate minimizer alias mappings
+ if (!this.isChild) {
+ for (var alias in this.context.aliases) {
+ this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
+ }
+ }
+
+ if (this.source[1]) {
+ this.source[1] = "var " + this.source[1].substring(2) + ";";
+ }
+
+ // Merge children
+ if (!this.isChild) {
+ this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
+ }
+
+ if (!this.environment.isSimple) {
+ this.source.push("return buffer;");
+ }
+
+ var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
+
+ for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
+ return this.topStackName();
+ },
+ topStackName: function() {
+ return "stack" + this.stackSlot;
+ },
+ flushInline: function() {
+ var inlineStack = this.inlineStack;
+ if (inlineStack.length) {
+ this.inlineStack = [];
+ for (var i = 0, len = inlineStack.length; i < len; i++) {
+ var entry = inlineStack[i];
+ if (entry instanceof Literal) {
+ this.compileStack.push(entry);
+ } else {
+ this.pushStack(entry);
+ }
+ }
+ }
+ },
+ isInline: function() {
+ return this.inlineStack.length;
+ },
+
+ popStack: function(wrapped) {
+ var inline = this.isInline(),
+ item = (inline ? this.inlineStack : this.compileStack).pop();
+
+ if (!wrapped && (item instanceof Literal)) {
+ return item.value;
+ } else {
+ if (!inline) {
+ this.stackSlot--;
+ }
+ return item;
+ }
+ },
+
+ topStack: function(wrapped) {
+ var stack = (this.isInline() ? this.inlineStack : this.compileStack),
+ item = stack[stack.length - 1];
+
+ if (!wrapped && (item instanceof Literal)) {
+ return item.value;
+ } else {
+ return item;
+ }
+ },
+
+ quotedString: function(str) {
+ return '"' + str
+ .replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"')
+ .replace(/\n/g, '\\n')
+ .replace(/\r/g, '\\r') + '"';
+ },
+
+ setupHelper: function(paramSize, name, missingParams) {
+ var params = [];
+ this.setupParams(paramSize, params, missingParams);
+ var foundHelper = this.nameLookup('helpers', name, 'helper');
+
+ return {
+ params: params,
+ name: foundHelper,
+ callParams: ["depth0"].concat(params).join(", "),
+ helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
+ };
+ },
+
+ // the params and contexts arguments are passed in arrays
+ // to fill in
+ setupParams: function(paramSize, params, useRegister) {
+ var options = [], contexts = [], types = [], param, inverse, program;
+
+ options.push("hash:" + this.popStack());
+
+ inverse = this.popStack();
+ program = this.popStack();
+
+ // Avoid setting fn and inverse if neither are set. This allows
+ // helpers to do a check for `if (options.fn)`
+ if (program || inverse) {
+ if (!program) {
+ this.context.aliases.self = "this";
+ program = "self.noop";
+ }
+
+ if (!inverse) {
+ this.context.aliases.self = "this";
+ inverse = "self.noop";
+ }
+
+ options.push("inverse:" + inverse);
+ options.push("fn:" + program);
+ }
+
+ for(var i=0; i', {
+ 'class': 'qtip-close ' + (options.style.widget ? '' : NAMESPACE+'-icon'),
+ 'title': close,
+ 'aria-label': close
+ })
+ .prepend(
+ $('', {
+ 'class': 'ui-icon ui-icon-close',
+ 'html': '×'
+ })
+ );
+ }
+
+ // Create button and setup attributes
+ elements.button.appendTo(elements.titlebar || tooltip)
+ .attr('role', 'button')
+ .click(function(event) {
+ if(!tooltip.hasClass(disabledClass)) { self.hide(event); }
+ return FALSE;
+ });
+ }
+
+ function createTitle()
+ {
+ var id = tooltipID+'-title';
+
+ // Destroy previous title element, if present
+ if(elements.titlebar) { removeTitle(); }
+
+ // Create title bar and title elements
+ elements.titlebar = $('', {
+ 'class': NAMESPACE + '-titlebar ' + (options.style.widget ? createWidgetClass('header') : '')
+ })
+ .append(
+ elements.title = $('', {
+ 'id': id,
+ 'class': NAMESPACE + '-title',
+ 'aria-atomic': TRUE
+ })
+ )
+ .insertBefore(elements.content)
+
+ // Button-specific events
+ .delegate('.qtip-close', 'mousedown keydown mouseup keyup mouseout', function(event) {
+ $(this).toggleClass('ui-state-active ui-state-focus', event.type.substr(-4) === 'down');
+ })
+ .delegate('.qtip-close', 'mouseover mouseout', function(event){
+ $(this).toggleClass('ui-state-hover', event.type === 'mouseover');
+ });
+
+ // Create button if enabled
+ if(options.content.title.button) { createButton(); }
+ }
+
+ function updateButton(button)
+ {
+ var elem = elements.button;
+
+ // Make sure tooltip is rendered and if not, return
+ if(!self.rendered) { return FALSE; }
+
+ if(!button) {
+ elem.remove();
+ }
+ else {
+ createButton();
+ }
+ }
+
+ function updateTitle(content, reposition)
+ {
+ var elem = elements.title;
+
+ // Make sure tooltip is rendered and if not, return
+ if(!self.rendered || !content) { return FALSE; }
+
+ // Use function to parse content
+ if($.isFunction(content)) {
+ content = content.call(target, cache.event, self);
+ }
+
+ // Remove title if callback returns false or null/undefined (but not '')
+ if(content === FALSE || (!content && content !== '')) { return removeTitle(FALSE); }
+
+ // Append new content if its a DOM array and show it if hidden
+ else if(content.jquery && content.length > 0) {
+ elem.empty().append(content.css({ display: 'block' }));
+ }
+
+ // Content is a regular string, insert the new content
+ else { elem.html(content); }
+
+ // Reposition if rnedered
+ if(reposition !== FALSE && self.rendered && tooltip[0].offsetWidth > 0) {
+ self.reposition(cache.event);
+ }
+ }
+
+ function deferredContent(deferred)
+ {
+ if(deferred && $.isFunction(deferred.done)) {
+ deferred.done(function(c) {
+ updateContent(c, null, FALSE);
+ });
+ }
+ }
+
+ function updateContent(content, reposition, checkDeferred)
+ {
+ var elem = elements.content;
+
+ // Make sure tooltip is rendered and content is defined. If not return
+ if(!self.rendered || !content) { return FALSE; }
+
+ // Use function to parse content
+ if($.isFunction(content)) {
+ content = content.call(target, cache.event, self) || '';
+ }
+
+ // Handle deferred content
+ if(checkDeferred !== FALSE) {
+ deferredContent(options.content.deferred);
+ }
+
+ // Append new content if its a DOM array and show it if hidden
+ if(content.jquery && content.length > 0) {
+ elem.empty().append(content.css({ display: 'block' }));
+ }
+
+ // Content is a regular string, insert the new content
+ else { elem.html(content); }
+
+ /*
+ * New images loaded detection method slimmed down from David DeSandro's plugin
+ * GitHub: https://github.com/desandro/imagesloaded/
+ */
+ function imagesLoaded(next) {
+ var elem = $(this),
+ images = elem.find('img').add( elem.filter('img') ),
+ loaded = [];
+
+ function imgLoaded( img ) {
+ // don't proceed if BLANKIMG image, or image is already loaded
+ if(img.src === BLANKIMG || $.inArray(img, loaded) !== -1) { return; }
+
+ // store element in loaded images array
+ loaded.push(img);
+
+ // cache image and its state for future calls
+ $.data(img, 'imagesLoaded', { src: img.src });
+
+ // call doneLoading and clean listeners if all images are loaded
+ if(images.length === loaded.length) {
+ setTimeout(next);
+ images.unbind('.imagesLoaded');
+ }
+ }
+
+ // No images? Proceed with next
+ if(!images.length) { return next(); }
+
+ images.bind('load.imagesLoaded error.imagesLoaded', function(event) {
+ imgLoaded(event.target);
+ })
+ .each(function(i, el) {
+ var src = el.src, cached = $.data(el, 'imagesLoaded');
+
+ /*
+ * Find out if this image has been already checked for status, and
+ * if it was and src has not changed, call imgLoaded on it. Also,
+ * if complete is true and browser supports natural sizes, try to
+ * check for image status manually
+ */
+ if((cached && cached.src === src) || (el.complete && el.naturalWidth)) {
+ imgLoaded(el);
+ }
+
+ /*
+ * Cached images don't fire load sometimes, so we reset src, but only when
+ * dealing with IE, or image is complete (loaded) and failed manual check
+ *
+ * Webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f
+ */
+ else if(el.readyState || el.complete) {
+ el.src = BLANKIMG; el.src = src;
+ }
+ });
+ }
+
+ /*
+ * If we're still rendering... insert into 'fx' queue our image dimension
+ * checker which will halt the showing of the tooltip until image dimensions
+ * can be detected properly.
+ */
+ if(self.rendered < 0) { tooltip.queue('fx', imagesLoaded); }
+
+ // We're fully rendered, so reset isDrawing flag and proceed without queue delay
+ else { isDrawing = 0; imagesLoaded.call(tooltip[0], $.noop); }
+
+ return self;
+ }
+
+ function assignEvents()
+ {
+ var posOptions = options.position,
+ targets = {
+ show: options.show.target,
+ hide: options.hide.target,
+ viewport: $(posOptions.viewport),
+ document: $(document),
+ body: $(document.body),
+ window: $(window)
+ },
+ events = {
+ show: $.trim('' + options.show.event).split(' '),
+ hide: $.trim('' + options.hide.event).split(' ')
+ },
+ IE6 = PLUGINS.ie === 6;
+
+ // Define show event method
+ function showMethod(event)
+ {
+ if(tooltip.hasClass(disabledClass)) { return FALSE; }
+
+ // Clear hide timers
+ clearTimeout(self.timers.show);
+ clearTimeout(self.timers.hide);
+
+ // Start show timer
+ var callback = function(){ self.toggle(TRUE, event); };
+ if(options.show.delay > 0) {
+ self.timers.show = setTimeout(callback, options.show.delay);
+ }
+ else{ callback(); }
+ }
+
+ // Define hide method
+ function hideMethod(event)
+ {
+ if(tooltip.hasClass(disabledClass) || isPositioning || isDrawing) { return FALSE; }
+
+ // Check if new target was actually the tooltip element
+ var relatedTarget = $(event.relatedTarget),
+ ontoTooltip = relatedTarget.closest(selector)[0] === tooltip[0],
+ ontoTarget = relatedTarget[0] === targets.show[0];
+
+ // Clear timers and stop animation queue
+ clearTimeout(self.timers.show);
+ clearTimeout(self.timers.hide);
+
+ // Prevent hiding if tooltip is fixed and event target is the tooltip. Or if mouse positioning is enabled and cursor momentarily overlaps
+ if(this !== relatedTarget[0] &&
+ (posOptions.target === 'mouse' && ontoTooltip) ||
+ (options.hide.fixed && (
+ (/mouse(out|leave|move)/).test(event.type) && (ontoTooltip || ontoTarget))
+ )) {
+ try { event.preventDefault(); event.stopImmediatePropagation(); } catch(e) {} return;
+ }
+
+ // If tooltip has displayed, start hide timer
+ if(options.hide.delay > 0) {
+ self.timers.hide = setTimeout(function(){ self.hide(event); }, options.hide.delay);
+ }
+ else{ self.hide(event); }
+ }
+
+ // Define inactive method
+ function inactiveMethod(event)
+ {
+ if(tooltip.hasClass(disabledClass)) { return FALSE; }
+
+ // Clear timer
+ clearTimeout(self.timers.inactive);
+ self.timers.inactive = setTimeout(function(){ self.hide(event); }, options.hide.inactive);
+ }
+
+ function repositionMethod(event) {
+ if(self.rendered && tooltip[0].offsetWidth > 0) { self.reposition(event); }
+ }
+
+ // On mouseenter/mouseleave...
+ tooltip.bind('mouseenter'+namespace+' mouseleave'+namespace, function(event) {
+ var state = event.type === 'mouseenter';
+
+ // Focus the tooltip on mouseenter (z-index stacking)
+ if(state) { self.focus(event); }
+
+ // Add hover class
+ tooltip.toggleClass(hoverClass, state);
+ });
+
+ // If using mouseout/mouseleave as a hide event...
+ if(/mouse(out|leave)/i.test(options.hide.event)) {
+ // Hide tooltips when leaving current window/frame (but not select/option elements)
+ if(options.hide.leave === 'window') {
+ targets.document.bind('mouseout'+namespace+' blur'+namespace, function(event) {
+ if(!/select|option/.test(event.target.nodeName) && !event.relatedTarget) {
+ self.hide(event);
+ }
+ });
+ }
+ }
+
+ // Enable hide.fixed
+ if(options.hide.fixed) {
+ // Add tooltip as a hide target
+ targets.hide = targets.hide.add(tooltip);
+
+ // Clear hide timer on tooltip hover to prevent it from closing
+ tooltip.bind('mouseover'+namespace, function() {
+ if(!tooltip.hasClass(disabledClass)) { clearTimeout(self.timers.hide); }
+ });
+ }
+
+ /*
+ * Make sure hoverIntent functions properly by using mouseleave to clear show timer if
+ * mouseenter/mouseout is used for show.event, even if it isn't in the users options.
+ */
+ else if(/mouse(over|enter)/i.test(options.show.event)) {
+ targets.hide.bind('mouseleave'+namespace, function(event) {
+ clearTimeout(self.timers.show);
+ });
+ }
+
+ // Hide tooltip on document mousedown if unfocus events are enabled
+ if(('' + options.hide.event).indexOf('unfocus') > -1) {
+ posOptions.container.closest('html').bind('mousedown'+namespace+' touchstart'+namespace, function(event) {
+ var elem = $(event.target),
+ enabled = self.rendered && !tooltip.hasClass(disabledClass) && tooltip[0].offsetWidth > 0,
+ isAncestor = elem.parents(selector).filter(tooltip[0]).length > 0;
+
+ if(elem[0] !== target[0] && elem[0] !== tooltip[0] && !isAncestor &&
+ !target.has(elem[0]).length && enabled
+ ) {
+ self.hide(event);
+ }
+ });
+ }
+
+ // Check if the tooltip hides when inactive
+ if('number' === typeof options.hide.inactive) {
+ // Bind inactive method to target as a custom event
+ targets.show.bind('qtip-'+id+'-inactive', inactiveMethod);
+
+ // Define events which reset the 'inactive' event handler
+ $.each(QTIP.inactiveEvents, function(index, type){
+ targets.hide.add(elements.tooltip).bind(type+namespace+'-inactive', inactiveMethod);
+ });
+ }
+
+ // Apply hide events
+ $.each(events.hide, function(index, type) {
+ var showIndex = $.inArray(type, events.show),
+ targetHide = $(targets.hide);
+
+ // Both events and targets are identical, apply events using a toggle
+ if((showIndex > -1 && targetHide.add(targets.show).length === targetHide.length) || type === 'unfocus')
+ {
+ targets.show.bind(type+namespace, function(event) {
+ if(tooltip[0].offsetWidth > 0) { hideMethod(event); }
+ else { showMethod(event); }
+ });
+
+ // Don't bind the event again
+ delete events.show[ showIndex ];
+ }
+
+ // Events are not identical, bind normally
+ else { targets.hide.bind(type+namespace, hideMethod); }
+ });
+
+ // Apply show events
+ $.each(events.show, function(index, type) {
+ targets.show.bind(type+namespace, showMethod);
+ });
+
+ // Check if the tooltip hides when mouse is moved a certain distance
+ if('number' === typeof options.hide.distance) {
+ // Bind mousemove to target to detect distance difference
+ targets.show.add(tooltip).bind('mousemove'+namespace, function(event) {
+ var origin = cache.origin || {},
+ limit = options.hide.distance,
+ abs = Math.abs;
+
+ // Check if the movement has gone beyond the limit, and hide it if so
+ if(abs(event.pageX - origin.pageX) >= limit || abs(event.pageY - origin.pageY) >= limit) {
+ self.hide(event);
+ }
+ });
+ }
+
+ // Mouse positioning events
+ if(posOptions.target === 'mouse') {
+ // Cache mousemove coords on show targets
+ targets.show.bind('mousemove'+namespace, storeMouse);
+
+ // If mouse adjustment is on...
+ if(posOptions.adjust.mouse) {
+ // Apply a mouseleave event so we don't get problems with overlapping
+ if(options.hide.event) {
+ // Hide when we leave the tooltip and not onto the show target
+ tooltip.bind('mouseleave'+namespace, function(event) {
+ if((event.relatedTarget || event.target) !== targets.show[0]) { self.hide(event); }
+ });
+
+ // Track if we're on the target or not
+ elements.target.bind('mouseenter'+namespace+' mouseleave'+namespace, function(event) {
+ cache.onTarget = event.type === 'mouseenter';
+ });
+ }
+
+ // Update tooltip position on mousemove
+ targets.document.bind('mousemove'+namespace, function(event) {
+ // Update the tooltip position only if the tooltip is visible and adjustment is enabled
+ if(self.rendered && cache.onTarget && !tooltip.hasClass(disabledClass) && tooltip[0].offsetWidth > 0) {
+ self.reposition(event || MOUSE);
+ }
+ });
+ }
+ }
+
+ // Adjust positions of the tooltip on window resize if enabled
+ if(posOptions.adjust.resize || targets.viewport.length) {
+ ($.event.special.resize ? targets.viewport : targets.window).bind('resize'+namespace, repositionMethod);
+ }
+
+ // Adjust tooltip position on scroll of the window or viewport element if present
+ if(posOptions.adjust.scroll) {
+ targets.window.add(posOptions.container).bind('scroll'+namespace, repositionMethod);
+ }
+ }
+
+ function unassignEvents()
+ {
+ var targets = [
+ options.show.target[0],
+ options.hide.target[0],
+ self.rendered && elements.tooltip[0],
+ options.position.container[0],
+ options.position.viewport[0],
+ options.position.container.closest('html')[0], // unfocus
+ window,
+ document
+ ];
+
+ // Check if tooltip is rendered
+ if(self.rendered) {
+ $([]).pushStack( $.grep(targets, function(i){ return typeof i === 'object'; }) ).unbind(namespace);
+ }
+
+ // Tooltip isn't yet rendered, remove render event
+ else { options.show.target.unbind(namespace+'-create'); }
+ }
+
+ // Setup builtin .set() option checks
+ self.checks.builtin = {
+ // Core checks
+ '^id$': function(obj, o, v) {
+ var id = v === TRUE ? QTIP.nextid : v,
+ tooltipID = NAMESPACE + '-' + id;
+
+ if(id !== FALSE && id.length > 0 && !$('#'+tooltipID).length) {
+ tooltip[0].id = tooltipID;
+ elements.content[0].id = tooltipID + '-content';
+ elements.title[0].id = tooltipID + '-title';
+ }
+ },
+
+ // Content checks
+ '^content.text$': function(obj, o, v) { updateContent(options.content.text); },
+ '^content.deferred$': function(obj, o, v) { deferredContent(options.content.deferred); },
+ '^content.title.text$': function(obj, o, v) {
+ // Remove title if content is null
+ if(!v) { return removeTitle(); }
+
+ // If title isn't already created, create it now and update
+ if(!elements.title && v) { createTitle(); }
+ updateTitle(v);
+ },
+ '^content.title.button$': function(obj, o, v){ updateButton(v); },
+
+ // Position checks
+ '^position.(my|at)$': function(obj, o, v){
+ // Parse new corner value into Corner objecct
+ if('string' === typeof v) {
+ obj[o] = new PLUGINS.Corner(v);
+ }
+ },
+ '^position.container$': function(obj, o, v){
+ if(self.rendered) { tooltip.appendTo(v); }
+ },
+
+ // Show checks
+ '^show.ready$': function() {
+ if(!self.rendered) { self.render(1); }
+ else { self.toggle(TRUE); }
+ },
+
+ // Style checks
+ '^style.classes$': function(obj, o, v) {
+ tooltip.attr('class', NAMESPACE + ' qtip ' + v);
+ },
+ '^style.width|height': function(obj, o, v) {
+ tooltip.css(o, v);
+ },
+ '^style.widget|content.title': setWidget,
+
+ // Events check
+ '^events.(render|show|move|hide|focus|blur)$': function(obj, o, v) {
+ tooltip[($.isFunction(v) ? '' : 'un') + 'bind']('tooltip'+o, v);
+ },
+
+ // Properties which require event reassignment
+ '^(show|hide|position).(event|target|fixed|inactive|leave|distance|viewport|adjust)': function() {
+ var posOptions = options.position;
+
+ // Set tracking flag
+ tooltip.attr('tracking', posOptions.target === 'mouse' && posOptions.adjust.mouse);
+
+ // Reassign events
+ unassignEvents(); assignEvents();
+ }
+ };
+
+ $.extend(self, {
+ /*
+ * Psuedo-private API methods
+ */
+ _triggerEvent: function(type, args, event)
+ {
+ var callback = $.Event('tooltip'+type);
+ callback.originalEvent = (event ? $.extend({}, event) : NULL) || cache.event || NULL;
+ tooltip.trigger(callback, [self].concat(args || []));
+
+ return !callback.isDefaultPrevented();
+ },
+
+ /*
+ * Public API methods
+ */
+ render: function(show)
+ {
+ if(self.rendered) { return self; } // If tooltip has already been rendered, exit
+
+ var text = options.content.text,
+ title = options.content.title,
+ posOptions = options.position;
+
+ // Add ARIA attributes to target
+ $.attr(target[0], 'aria-describedby', tooltipID);
+
+ // Create tooltip element
+ tooltip = elements.tooltip = $('', {
+ 'id': tooltipID,
+ 'class': [ NAMESPACE, defaultClass, options.style.classes, NAMESPACE + '-pos-' + options.position.my.abbrev() ].join(' '),
+ 'width': options.style.width || '',
+ 'height': options.style.height || '',
+ 'tracking': posOptions.target === 'mouse' && posOptions.adjust.mouse,
+
+ /* ARIA specific attributes */
+ 'role': 'alert',
+ 'aria-live': 'polite',
+ 'aria-atomic': FALSE,
+ 'aria-describedby': tooltipID + '-content',
+ 'aria-hidden': TRUE
+ })
+ .toggleClass(disabledClass, cache.disabled)
+ .data('qtip', self)
+ .appendTo(options.position.container)
+ .append(
+ // Create content element
+ elements.content = $('', {
+ 'class': NAMESPACE + '-content',
+ 'id': tooltipID + '-content',
+ 'aria-atomic': TRUE
+ })
+ );
+
+ // Set rendered flag and prevent redundant reposition calls for now
+ self.rendered = -1;
+ isPositioning = 1;
+
+ // Create title...
+ if(title.text) {
+ createTitle();
+
+ // Update title only if its not a callback (called in toggle if so)
+ if(!$.isFunction(title.text)) { updateTitle(title.text, FALSE); }
+ }
+
+ // Create button
+ else if(title.button) { createButton(); }
+
+ // Set proper rendered flag and update content if not a callback function (called in toggle)
+ if(!$.isFunction(text) || text.then) { updateContent(text, FALSE); }
+ self.rendered = TRUE;
+
+ // Setup widget classes
+ setWidget();
+
+ // Assign passed event callbacks (before plugins!)
+ $.each(options.events, function(name, callback) {
+ if($.isFunction(callback)) {
+ tooltip.bind(name === 'toggle' ? 'tooltipshow tooltiphide' : 'tooltip'+name, callback);
+ }
+ });
+
+ // Initialize 'render' plugins
+ $.each(PLUGINS, function() {
+ if(this.initialize === 'render') { this(self); }
+ });
+
+ // Assign events
+ assignEvents();
+
+ /* Queue this part of the render process in our fx queue so we can
+ * load images before the tooltip renders fully.
+ *
+ * See: updateContent method
+ */
+ tooltip.queue('fx', function(next) {
+ // tooltiprender event
+ self._triggerEvent('render');
+
+ // Reset flags
+ isPositioning = 0;
+
+ // Show tooltip if needed
+ if(options.show.ready || show) {
+ self.toggle(TRUE, cache.event, FALSE);
+ }
+
+ next(); // Move on to next method in queue
+ });
+
+ return self;
+ },
+
+ get: function(notation)
+ {
+ var result, o;
+
+ switch(notation.toLowerCase())
+ {
+ case 'dimensions':
+ result = {
+ height: tooltip.outerHeight(FALSE),
+ width: tooltip.outerWidth(FALSE)
+ };
+ break;
+
+ case 'offset':
+ result = PLUGINS.offset(tooltip, options.position.container);
+ break;
+
+ default:
+ o = convertNotation(notation.toLowerCase());
+ result = o[0][ o[1] ];
+ result = result.precedance ? result.string() : result;
+ break;
+ }
+
+ return result;
+ },
+
+ set: function(option, value)
+ {
+ var rmove = /^position\.(my|at|adjust|target|container)|style|content|show\.ready/i,
+ rdraw = /^content\.(title|attr)|style/i,
+ reposition = FALSE,
+ checks = self.checks,
+ name;
+
+ function callback(notation, args) {
+ var category, rule, match;
+
+ for(category in checks) {
+ for(rule in checks[category]) {
+ if(match = (new RegExp(rule, 'i')).exec(notation)) {
+ args.push(match);
+ checks[category][rule].apply(self, args);
+ }
+ }
+ }
+ }
+
+ // Convert singular option/value pair into object form
+ if('string' === typeof option) {
+ name = option; option = {}; option[name] = value;
+ }
+ else { option = $.extend(TRUE, {}, option); }
+
+ // Set all of the defined options to their new values
+ $.each(option, function(notation, value) {
+ var obj = convertNotation( notation.toLowerCase() ), previous;
+
+ // Set new obj value
+ previous = obj[0][ obj[1] ];
+ obj[0][ obj[1] ] = 'object' === typeof value && value.nodeType ? $(value) : value;
+
+ // Set the new params for the callback
+ option[notation] = [obj[0], obj[1], value, previous];
+
+ // Also check if we need to reposition
+ reposition = rmove.test(notation) || reposition;
+ });
+
+ // Re-sanitize options
+ sanitizeOptions(options);
+
+ /*
+ * Execute any valid callbacks for the set options
+ * Also set isPositioning/isDrawing so we don't get loads of redundant repositioning calls.
+ */
+ isPositioning = 1; $.each(option, callback); isPositioning = 0;
+
+ // Update position if needed
+ if(self.rendered && tooltip[0].offsetWidth > 0 && reposition) {
+ self.reposition( options.position.target === 'mouse' ? NULL : cache.event );
+ }
+
+ return self;
+ },
+
+ toggle: function(state, event)
+ {
+ // Try to prevent flickering when tooltip overlaps show element
+ if(event) {
+ if((/over|enter/).test(event.type) && (/out|leave/).test(cache.event.type) &&
+ options.show.target.add(event.target).length === options.show.target.length &&
+ tooltip.has(event.relatedTarget).length) {
+ return self;
+ }
+
+ // Cache event
+ cache.event = $.extend({}, event);
+ }
+
+ // Render the tooltip if showing and it isn't already
+ if(!self.rendered) { return state ? self.render(1) : self; }
+
+ var type = state ? 'show' : 'hide',
+ opts = options[type],
+ otherOpts = options[ !state ? 'show' : 'hide' ],
+ posOptions = options.position,
+ contentOptions = options.content,
+ width = tooltip.css('width'),
+ visible = tooltip[0].offsetWidth > 0,
+ animate = state || opts.target.length === 1,
+ sameTarget = !event || opts.target.length < 2 || cache.target[0] === event.target,
+ showEvent, delay;
+
+ // Detect state if valid one isn't provided
+ if((typeof state).search('boolean|number')) { state = !visible; }
+
+ // Return if element is already in correct state
+ if(!tooltip.is(':animated') && visible === state && sameTarget) { return self; }
+
+ // tooltipshow/tooltiphide events
+ if(!self._triggerEvent(type, [90]) && !self.destroyed) { return self; }
+
+ // Set ARIA hidden status attribute
+ $.attr(tooltip[0], 'aria-hidden', !!!state);
+
+ // Execute state specific properties
+ if(state) {
+ // Store show origin coordinates
+ cache.origin = $.extend({}, MOUSE);
+
+ // Focus the tooltip
+ self.focus(event);
+
+ // Update tooltip content & title if it's a dynamic function
+ if($.isFunction(contentOptions.text)) { updateContent(contentOptions.text, FALSE); }
+ if($.isFunction(contentOptions.title.text)) { updateTitle(contentOptions.title.text, FALSE); }
+
+ // Cache mousemove events for positioning purposes (if not already tracking)
+ if(!trackingBound && posOptions.target === 'mouse' && posOptions.adjust.mouse) {
+ $(document).bind('mousemove.qtip', storeMouse);
+ trackingBound = TRUE;
+ }
+
+ // Update the tooltip position (set width first to prevent viewport/max-width issues)
+ if(!width) { tooltip.css('width', tooltip.outerWidth()); }
+ self.reposition(event, arguments[2]);
+ if(!width) { tooltip.css('width', ''); }
+
+ // Hide other tooltips if tooltip is solo
+ if(!!opts.solo) {
+ (typeof opts.solo === 'string' ? $(opts.solo) : $(selector, opts.solo))
+ .not(tooltip).not(opts.target).qtip('hide', $.Event('tooltipsolo'));
+ }
+ }
+ else {
+ // Clear show timer if we're hiding
+ clearTimeout(self.timers.show);
+
+ // Remove cached origin on hide
+ delete cache.origin;
+
+ // Remove mouse tracking event if not needed (all tracking qTips are hidden)
+ if(trackingBound && !$(selector+'[tracking="true"]:visible', opts.solo).not(tooltip).length) {
+ $(document).unbind('mousemove.qtip');
+ trackingBound = FALSE;
+ }
+
+ // Blur the tooltip
+ self.blur(event);
+ }
+
+ // Define post-animation, state specific properties
+ function after() {
+ if(state) {
+ // Prevent antialias from disappearing in IE by removing filter
+ if(PLUGINS.ie) { tooltip[0].style.removeAttribute('filter'); }
+
+ // Remove overflow setting to prevent tip bugs
+ tooltip.css('overflow', '');
+
+ // Autofocus elements if enabled
+ if('string' === typeof opts.autofocus) {
+ $(opts.autofocus, tooltip).focus();
+ }
+
+ // If set, hide tooltip when inactive for delay period
+ opts.target.trigger('qtip-'+id+'-inactive');
+ }
+ else {
+ // Reset CSS states
+ tooltip.css({
+ display: '',
+ visibility: '',
+ opacity: '',
+ left: '',
+ top: ''
+ });
+ }
+
+ // tooltipvisible/tooltiphidden events
+ self._triggerEvent(state ? 'visible' : 'hidden');
+ }
+
+ // If no effect type is supplied, use a simple toggle
+ if(opts.effect === FALSE || animate === FALSE) {
+ tooltip[ type ]();
+ after.call(tooltip);
+ }
+
+ // Use custom function if provided
+ else if($.isFunction(opts.effect)) {
+ tooltip.stop(1, 1);
+ opts.effect.call(tooltip, self);
+ tooltip.queue('fx', function(n){ after(); n(); });
+ }
+
+ // Use basic fade function by default
+ else { tooltip.fadeTo(90, state ? 1 : 0, after); }
+
+ // If inactive hide method is set, active it
+ if(state) { opts.target.trigger('qtip-'+id+'-inactive'); }
+
+ return self;
+ },
+
+ show: function(event){ return self.toggle(TRUE, event); },
+
+ hide: function(event){ return self.toggle(FALSE, event); },
+
+ focus: function(event)
+ {
+ if(!self.rendered) { return self; }
+
+ var qtips = $(selector),
+ curIndex = parseInt(tooltip[0].style.zIndex, 10),
+ newIndex = QTIP.zindex + qtips.length,
+ cachedEvent = $.extend({}, event),
+ focusedElem;
+
+ // Only update the z-index if it has changed and tooltip is not already focused
+ if(!tooltip.hasClass(focusClass))
+ {
+ // tooltipfocus event
+ if(self._triggerEvent('focus', [newIndex], cachedEvent)) {
+ // Only update z-index's if they've changed
+ if(curIndex !== newIndex) {
+ // Reduce our z-index's and keep them properly ordered
+ qtips.each(function() {
+ if(this.style.zIndex > curIndex) {
+ this.style.zIndex = this.style.zIndex - 1;
+ }
+ });
+
+ // Fire blur event for focused tooltip
+ qtips.filter('.' + focusClass).qtip('blur', cachedEvent);
+ }
+
+ // Set the new z-index
+ tooltip.addClass(focusClass)[0].style.zIndex = newIndex;
+ }
+ }
+
+ return self;
+ },
+
+ blur: function(event) {
+ // Set focused status to FALSE
+ tooltip.removeClass(focusClass);
+
+ // tooltipblur event
+ self._triggerEvent('blur', [tooltip.css('zIndex')], event);
+
+ return self;
+ },
+
+ reposition: function(event, effect)
+ {
+ if(!self.rendered || isPositioning) { return self; }
+
+ // Set positioning flag
+ isPositioning = 1;
+
+ var target = options.position.target,
+ posOptions = options.position,
+ my = posOptions.my,
+ at = posOptions.at,
+ adjust = posOptions.adjust,
+ method = adjust.method.split(' '),
+ elemWidth = tooltip.outerWidth(FALSE),
+ elemHeight = tooltip.outerHeight(FALSE),
+ targetWidth = 0,
+ targetHeight = 0,
+ type = tooltip.css('position'),
+ viewport = posOptions.viewport,
+ position = { left: 0, top: 0 },
+ container = posOptions.container,
+ visible = tooltip[0].offsetWidth > 0,
+ isScroll = event && event.type === 'scroll',
+ win = $(window),
+ adjusted, offset;
+
+ // Check if absolute position was passed
+ if($.isArray(target) && target.length === 2) {
+ // Force left top and set position
+ at = { x: LEFT, y: TOP };
+ position = { left: target[0], top: target[1] };
+ }
+
+ // Check if mouse was the target
+ else if(target === 'mouse' && ((event && event.pageX) || cache.event.pageX)) {
+ // Force left top to allow flipping
+ at = { x: LEFT, y: TOP };
+
+ // Use cached event if one isn't available for positioning
+ event = MOUSE && MOUSE.pageX && (adjust.mouse || !event || !event.pageX) ? { pageX: MOUSE.pageX, pageY: MOUSE.pageY } :
+ (event && (event.type === 'resize' || event.type === 'scroll') ? cache.event :
+ event && event.pageX && event.type === 'mousemove' ? event :
+ (!adjust.mouse || options.show.distance) && cache.origin && cache.origin.pageX ? cache.origin :
+ event) || event || cache.event || MOUSE || {};
+
+ // Use event coordinates for position
+ if(type !== 'static') { position = container.offset(); }
+ position = { left: event.pageX - position.left, top: event.pageY - position.top };
+
+ // Scroll events are a pain, some browsers
+ if(adjust.mouse && isScroll) {
+ position.left -= MOUSE.scrollX - win.scrollLeft();
+ position.top -= MOUSE.scrollY - win.scrollTop();
+ }
+ }
+
+ // Target wasn't mouse or absolute...
+ else {
+ // Check if event targetting is being used
+ if(target === 'event' && event && event.target && event.type !== 'scroll' && event.type !== 'resize') {
+ cache.target = $(event.target);
+ }
+ else if(target !== 'event'){
+ cache.target = $(target.jquery ? target : elements.target);
+ }
+ target = cache.target;
+
+ // Parse the target into a jQuery object and make sure there's an element present
+ target = $(target).eq(0);
+ if(target.length === 0) { return self; }
+
+ // Check if window or document is the target
+ else if(target[0] === document || target[0] === window) {
+ targetWidth = PLUGINS.iOS ? window.innerWidth : target.width();
+ targetHeight = PLUGINS.iOS ? window.innerHeight : target.height();
+
+ if(target[0] === window) {
+ position = {
+ top: (viewport || target).scrollTop(),
+ left: (viewport || target).scrollLeft()
+ };
+ }
+ }
+
+ // Check if the target is an element
+ else if(PLUGINS.imagemap && target.is('area')) {
+ adjusted = PLUGINS.imagemap(self, target, at, PLUGINS.viewport ? method : FALSE);
+ }
+
+ // Check if the target is an SVG element
+ else if(PLUGINS.svg && target[0].ownerSVGElement) {
+ adjusted = PLUGINS.svg(self, target, at, PLUGINS.viewport ? method : FALSE);
+ }
+
+ // Otherwise use regular jQuery methods
+ else {
+ targetWidth = target.outerWidth(FALSE);
+ targetHeight = target.outerHeight(FALSE);
+ position = target.offset();
+ }
+
+ // Parse returned plugin values into proper variables
+ if(adjusted) {
+ targetWidth = adjusted.width;
+ targetHeight = adjusted.height;
+ offset = adjusted.offset;
+ position = adjusted.position;
+ }
+
+
+ // Adjust position to take into account offset parents
+ position = PLUGINS.offset(target, position, container);
+
+ // Adjust for position.fixed tooltips (and also iOS scroll bug in v3.2-4.0 & v4.3-4.3.2)
+ if((PLUGINS.iOS > 3.1 && PLUGINS.iOS < 4.1) ||
+ (PLUGINS.iOS >= 4.3 && PLUGINS.iOS < 4.33) ||
+ (!PLUGINS.iOS && type === 'fixed')
+ ){
+ position.left -= win.scrollLeft();
+ position.top -= win.scrollTop();
+ }
+
+ // Adjust position relative to target
+ position.left += at.x === RIGHT ? targetWidth : at.x === CENTER ? targetWidth / 2 : 0;
+ position.top += at.y === BOTTOM ? targetHeight : at.y === CENTER ? targetHeight / 2 : 0;
+ }
+
+ // Adjust position relative to tooltip
+ position.left += adjust.x + (my.x === RIGHT ? -elemWidth : my.x === CENTER ? -elemWidth / 2 : 0);
+ position.top += adjust.y + (my.y === BOTTOM ? -elemHeight : my.y === CENTER ? -elemHeight / 2 : 0);
+
+ // Use viewport adjustment plugin if enabled
+ if(PLUGINS.viewport) {
+ position.adjusted = PLUGINS.viewport(
+ self, position, posOptions, targetWidth, targetHeight, elemWidth, elemHeight
+ );
+
+ // Apply offsets supplied by positioning plugin (if used)
+ if(offset && position.adjusted.left) { position.left += offset.left; }
+ if(offset && position.adjusted.top) { position.top += offset.top; }
+ }
+
+ // Viewport adjustment is disabled, set values to zero
+ else { position.adjusted = { left: 0, top: 0 }; }
+
+ // tooltipmove event
+ if(!self._triggerEvent('move', [position, viewport.elem || viewport], event)) { return self; }
+ delete position.adjusted;
+
+ // If effect is disabled, target it mouse, no animation is defined or positioning gives NaN out, set CSS directly
+ if(effect === FALSE || !visible || isNaN(position.left) || isNaN(position.top) || target === 'mouse' || !$.isFunction(posOptions.effect)) {
+ tooltip.css(position);
+ }
+
+ // Use custom function if provided
+ else if($.isFunction(posOptions.effect)) {
+ posOptions.effect.call(tooltip, self, $.extend({}, position));
+ tooltip.queue(function(next) {
+ // Reset attributes to avoid cross-browser rendering bugs
+ $(this).css({ opacity: '', height: '' });
+ if(PLUGINS.ie) { this.style.removeAttribute('filter'); }
+
+ next();
+ });
+ }
+
+ // Set positioning flagwtf
+ isPositioning = 0;
+
+ return self;
+ },
+
+ disable: function(state)
+ {
+ if('boolean' !== typeof state) {
+ state = !(tooltip.hasClass(disabledClass) || cache.disabled);
+ }
+
+ if(self.rendered) {
+ tooltip.toggleClass(disabledClass, state);
+ $.attr(tooltip[0], 'aria-disabled', state);
+ }
+ else {
+ cache.disabled = !!state;
+ }
+
+ return self;
+ },
+
+ enable: function() { return self.disable(FALSE); },
+
+ destroy: function(immediate)
+ {
+ // Set flag the signify destroy is taking place to plugins
+ // and ensure it only gets destroyed once!
+ if(self.destroyed) { return; }
+ self.destroyed = TRUE;
+
+ function process() {
+ var t = target[0],
+ title = $.attr(t, oldtitle),
+ elemAPI = target.data('qtip');
+
+ // Destroy tooltip and any associated plugins if rendered
+ if(self.rendered) {
+ // Destroy all plugins
+ $.each(self.plugins, function(name) {
+ if(this.destroy) { this.destroy(); }
+ delete self.plugins[name];
+ });
+
+ // Remove all descendants and tooltip element
+ tooltip.stop(1,0).find('*').remove().end().remove();
+
+ // Set rendered flag
+ self.rendered = FALSE;
+ }
+
+ // Clear timers and remove bound events
+ clearTimeout(self.timers.show);
+ clearTimeout(self.timers.hide);
+ unassignEvents();
+
+ // If the API if actually this qTip API...
+ if(!elemAPI || self === elemAPI) {
+ // Remove api object
+ target.removeData('qtip').removeAttr(HASATTR);
+
+ // Reset old title attribute if removed
+ if(options.suppress && title) {
+ target.attr('title', title);
+ target.removeAttr(oldtitle);
+ }
+
+ // Remove ARIA attributes
+ target.removeAttr('aria-describedby');
+ }
+
+ // Remove qTip events associated with this API
+ target.unbind('.qtip-'+id);
+
+ // Remove ID from used id objects, and delete object references
+ // for better garbage collection and leak protection
+ delete usedIDs[self.id];
+ delete self.options; delete self.elements;
+ delete self.cache; delete self.timers;
+ delete self.checks;
+ }
+
+ var isHiding = FALSE;
+
+ // If an immediate destory is needed
+ if(immediate !== TRUE) {
+ // Check to see if the hide call below suceeds
+ tooltip.bind('tooltiphide', function() {
+ // Set the hiding flag and process on hidden
+ isHiding = TRUE;
+ tooltip.bind('tooltiphidden', process);
+ });
+ self.hide();
+ }
+
+ // If we're not in the process of hiding... process
+ if(!isHiding) { process(); }
+
+ return target;
+ }
+ });
+}
+
+// Initialization method
+function init(elem, id, opts)
+{
+ var obj, posOptions, attr, config, title,
+
+ // Setup element references
+ docBody = $(document.body),
+
+ // Use document body instead of document element if needed
+ newTarget = elem[0] === document ? docBody : elem,
+
+ // Grab metadata from element if plugin is present
+ metadata = (elem.metadata) ? elem.metadata(opts.metadata) : NULL,
+
+ // If metadata type if HTML5, grab 'name' from the object instead, or use the regular data object otherwise
+ metadata5 = opts.metadata.type === 'html5' && metadata ? metadata[opts.metadata.name] : NULL,
+
+ // Grab data from metadata.name (or data-qtipopts as fallback) using .data() method,
+ html5 = elem.data(opts.metadata.name || 'qtipopts');
+
+ // If we don't get an object returned attempt to parse it manualyl without parseJSON
+ try { html5 = typeof html5 === 'string' ? $.parseJSON(html5) : html5; } catch(e) {}
+
+ // Merge in and sanitize metadata
+ config = $.extend(TRUE, {}, QTIP.defaults, opts,
+ typeof html5 === 'object' ? sanitizeOptions(html5) : NULL,
+ sanitizeOptions(metadata5 || metadata));
+
+ // Re-grab our positioning options now we've merged our metadata and set id to passed value
+ posOptions = config.position;
+ config.id = id;
+
+ // Setup missing content if none is detected
+ if('boolean' === typeof config.content.text) {
+ attr = elem.attr(config.content.attr);
+
+ // Grab from supplied attribute if available
+ if(config.content.attr !== FALSE && attr) { config.content.text = attr; }
+
+ // No valid content was found, abort render
+ else { return FALSE; }
+ }
+
+ // Setup target options
+ if(!posOptions.container.length) { posOptions.container = docBody; }
+ if(posOptions.target === FALSE) { posOptions.target = newTarget; }
+ if(config.show.target === FALSE) { config.show.target = newTarget; }
+ if(config.show.solo === TRUE) { config.show.solo = posOptions.container.closest('body'); }
+ if(config.hide.target === FALSE) { config.hide.target = newTarget; }
+ if(config.position.viewport === TRUE) { config.position.viewport = posOptions.container; }
+
+ // Ensure we only use a single container
+ posOptions.container = posOptions.container.eq(0);
+
+ // Convert position corner values into x and y strings
+ posOptions.at = new PLUGINS.Corner(posOptions.at);
+ posOptions.my = new PLUGINS.Corner(posOptions.my);
+
+ // Destroy previous tooltip if overwrite is enabled, or skip element if not
+ if(elem.data('qtip')) {
+ if(config.overwrite) {
+ elem.qtip('destroy');
+ }
+ else if(config.overwrite === FALSE) {
+ return FALSE;
+ }
+ }
+
+ // Add has-qtip attribute
+ elem.attr(HASATTR, true);
+
+ // Remove title attribute and store it if present
+ if(config.suppress && (title = elem.attr('title'))) {
+ // Final attr call fixes event delegatiom and IE default tooltip showing problem
+ elem.removeAttr('title').attr(oldtitle, title).attr('title', '');
+ }
+
+ // Initialize the tooltip and add API reference
+ obj = new QTip(elem, config, id, !!attr);
+ elem.data('qtip', obj);
+
+ // Catch remove/removeqtip events on target element to destroy redundant tooltip
+ elem.one('remove.qtip-'+id+' removeqtip.qtip-'+id, function() {
+ var api; if((api = $(this).data('qtip'))) { api.destroy(); }
+ });
+
+ return obj;
+}
+
+// jQuery $.fn extension method
+QTIP = $.fn.qtip = function(options, notation, newValue)
+{
+ var command = ('' + options).toLowerCase(), // Parse command
+ returned = NULL,
+ args = $.makeArray(arguments).slice(1),
+ event = args[args.length - 1],
+ opts = this[0] ? $.data(this[0], 'qtip') : NULL;
+
+ // Check for API request
+ if((!arguments.length && opts) || command === 'api') {
+ return opts;
+ }
+
+ // Execute API command if present
+ else if('string' === typeof options)
+ {
+ this.each(function()
+ {
+ var api = $.data(this, 'qtip');
+ if(!api) { return TRUE; }
+
+ // Cache the event if possible
+ if(event && event.timeStamp) { api.cache.event = event; }
+
+ // Check for specific API commands
+ if((command === 'option' || command === 'options') && notation) {
+ if($.isPlainObject(notation) || newValue !== undefined) {
+ api.set(notation, newValue);
+ }
+ else {
+ returned = api.get(notation);
+ return FALSE;
+ }
+ }
+
+ // Execute API command
+ else if(api[command]) {
+ api[command].apply(api[command], args);
+ }
+ });
+
+ return returned !== NULL ? returned : this;
+ }
+
+ // No API commands. validate provided options and setup qTips
+ else if('object' === typeof options || !arguments.length)
+ {
+ opts = sanitizeOptions($.extend(TRUE, {}, options));
+
+ // Bind the qTips
+ return QTIP.bind.call(this, opts, event);
+ }
+};
+
+// $.fn.qtip Bind method
+QTIP.bind = function(opts, event)
+{
+ return this.each(function(i) {
+ var options, targets, events, namespace, api, id;
+
+ // Find next available ID, or use custom ID if provided
+ id = $.isArray(opts.id) ? opts.id[i] : opts.id;
+ id = !id || id === FALSE || id.length < 1 || usedIDs[id] ? QTIP.nextid++ : (usedIDs[id] = id);
+
+ // Setup events namespace
+ namespace = '.qtip-'+id+'-create';
+
+ // Initialize the qTip and re-grab newly sanitized options
+ api = init($(this), id, opts);
+ if(api === FALSE) { return TRUE; }
+ options = api.options;
+
+ // Initialize plugins
+ $.each(PLUGINS, function() {
+ if(this.initialize === 'initialize') { this(api); }
+ });
+
+ // Determine hide and show targets
+ targets = { show: options.show.target, hide: options.hide.target };
+ events = {
+ show: $.trim('' + options.show.event).replace(/ /g, namespace+' ') + namespace,
+ hide: $.trim('' + options.hide.event).replace(/ /g, namespace+' ') + namespace
+ };
+
+ /*
+ * Make sure hoverIntent functions properly by using mouseleave as a hide event if
+ * mouseenter/mouseout is used for show.event, even if it isn't in the users options.
+ */
+ if(/mouse(over|enter)/i.test(events.show) && !/mouse(out|leave)/i.test(events.hide)) {
+ events.hide += ' mouseleave' + namespace;
+ }
+
+ /*
+ * Also make sure initial mouse targetting works correctly by caching mousemove coords
+ * on show targets before the tooltip has rendered.
+ *
+ * Also set onTarget when triggered to keep mouse tracking working
+ */
+ targets.show.bind('mousemove'+namespace, function(event) {
+ storeMouse(event);
+ api.cache.onTarget = TRUE;
+ });
+
+ // Define hoverIntent function
+ function hoverIntent(event) {
+ function render() {
+ // Cache mouse coords,render and render the tooltip
+ api.render(typeof event === 'object' || options.show.ready);
+
+ // Unbind show and hide events
+ targets.show.add(targets.hide).unbind(namespace);
+ }
+
+ // Only continue if tooltip isn't disabled
+ if(api.cache.disabled) { return FALSE; }
+
+ // Cache the event data
+ api.cache.event = $.extend({}, event);
+ api.cache.target = event ? $(event.target) : [undefined];
+
+ // Start the event sequence
+ if(options.show.delay > 0) {
+ clearTimeout(api.timers.show);
+ api.timers.show = setTimeout(render, options.show.delay);
+ if(events.show !== events.hide) {
+ targets.hide.bind(events.hide, function() { clearTimeout(api.timers.show); });
+ }
+ }
+ else { render(); }
+ }
+
+ // Bind show events to target
+ targets.show.bind(events.show, hoverIntent);
+
+ // Prerendering is enabled, create tooltip now
+ if(options.show.ready || options.prerender) { hoverIntent(event); }
+ });
+};
+
+// Setup base plugins
+PLUGINS = QTIP.plugins = {
+ // Corner object parser
+ Corner: function(corner) {
+ corner = ('' + corner).replace(/([A-Z])/, ' $1').replace(/middle/gi, CENTER).toLowerCase();
+ this.x = (corner.match(/left|right/i) || corner.match(/center/) || ['inherit'])[0].toLowerCase();
+ this.y = (corner.match(/top|bottom|center/i) || ['inherit'])[0].toLowerCase();
+
+ var f = corner.charAt(0); this.precedance = (f === 't' || f === 'b' ? Y : X);
+
+ this.string = function() { return this.precedance === Y ? this.y+this.x : this.x+this.y; };
+ this.abbrev = function() {
+ var x = this.x.substr(0,1), y = this.y.substr(0,1);
+ return x === y ? x : this.precedance === Y ? y + x : x + y;
+ };
+
+ this.invertx = function(center) { this.x = this.x === LEFT ? RIGHT : this.x === RIGHT ? LEFT : center || this.x; };
+ this.inverty = function(center) { this.y = this.y === TOP ? BOTTOM : this.y === BOTTOM ? TOP : center || this.y; };
+
+ this.clone = function() {
+ return {
+ x: this.x, y: this.y, precedance: this.precedance,
+ string: this.string, abbrev: this.abbrev, clone: this.clone,
+ invertx: this.invertx, inverty: this.inverty
+ };
+ };
+ },
+
+ // Custom (more correct for qTip!) offset calculator
+ offset: function(elem, pos, container) {
+ var docBody = elem.closest('body'),
+ quirks = PLUGINS.ie && document.compatMode !== 'CSS1Compat',
+ parent = container, scrolled,
+ coffset, overflow;
+
+ function scroll(e, i) {
+ pos.left += i * e.scrollLeft();
+ pos.top += i * e.scrollTop();
+ }
+
+ if(parent) {
+ // Compensate for non-static containers offset
+ do {
+ if(parent.css('position') !== 'static') {
+ coffset = parent.position();
+
+ // Account for element positioning, borders and margins
+ pos.left -= coffset.left + (parseInt(parent.css('borderLeftWidth'), 10) || 0) + (parseInt(parent.css('marginLeft'), 10) || 0);
+ pos.top -= coffset.top + (parseInt(parent.css('borderTopWidth'), 10) || 0) + (parseInt(parent.css('marginTop'), 10) || 0);
+
+ // If this is the first parent element with an overflow of "scroll" or "auto", store it
+ if(!scrolled && (overflow = parent.css('overflow')) !== 'hidden' && overflow !== 'visible') { scrolled = parent; }
+ }
+ }
+ while((parent = $(parent[0].offsetParent)).length);
+
+ // Compensate for containers scroll if it also has an offsetParent (or in IE quirks mode)
+ if(scrolled && scrolled[0] !== docBody[0] || quirks) {
+ scroll( scrolled || docBody, 1 );
+ }
+ }
+
+ return pos;
+ },
+
+ /*
+ * IE version detection
+ *
+ * Adapted from: http://ajaxian.com/archives/attack-of-the-ie-conditional-comment
+ * Credit to James Padolsey for the original implemntation!
+ */
+ ie: (function(){
+ var v = 3, div = document.createElement('div');
+ while ((div.innerHTML = '')) {
+ if(!div.getElementsByTagName('i')[0]) { break; }
+ }
+ return v > 4 ? v : FALSE;
+ }()),
+
+ /*
+ * iOS version detection
+ */
+ iOS: parseFloat(
+ ('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0,''])[1])
+ .replace('undefined', '3_2').replace('_', '.').replace('_', '')
+ ) || FALSE,
+
+ /*
+ * jQuery-specific $.fn overrides
+ */
+ fn: {
+ /* Allow other plugins to successfully retrieve the title of an element with a qTip applied */
+ attr: function(attr, val) {
+ if(this.length) {
+ var self = this[0],
+ title = 'title',
+ api = $.data(self, 'qtip');
+
+ if(attr === title && api && 'object' === typeof api && api.options.suppress) {
+ if(arguments.length < 2) {
+ return $.attr(self, oldtitle);
+ }
+
+ // If qTip is rendered and title was originally used as content, update it
+ if(api && api.options.content.attr === title && api.cache.attr) {
+ api.set('content.text', val);
+ }
+
+ // Use the regular attr method to set, then cache the result
+ return this.attr(oldtitle, val);
+ }
+ }
+
+ return $.fn['attr'+replaceSuffix].apply(this, arguments);
+ },
+
+ /* Allow clone to correctly retrieve cached title attributes */
+ clone: function(keepData) {
+ var titles = $([]), title = 'title',
+
+ // Clone our element using the real clone method
+ elems = $.fn['clone'+replaceSuffix].apply(this, arguments);
+
+ // Grab all elements with an oldtitle set, and change it to regular title attribute, if keepData is false
+ if(!keepData) {
+ elems.filter('['+oldtitle+']').attr('title', function() {
+ return $.attr(this, oldtitle);
+ })
+ .removeAttr(oldtitle);
+ }
+
+ return elems;
+ }
+ }
+};
+
+// Apply the fn overrides above
+$.each(PLUGINS.fn, function(name, func) {
+ if(!func || $.fn[name+replaceSuffix]) { return TRUE; }
+
+ var old = $.fn[name+replaceSuffix] = $.fn[name];
+ $.fn[name] = function() {
+ return func.apply(this, arguments) || old.apply(this, arguments);
+ };
+});
+
+/* Fire off 'removeqtip' handler in $.cleanData if jQuery UI not present (it already does similar).
+ * This snippet is taken directly from jQuery UI source code found here:
+ * http://code.jquery.com/ui/jquery-ui-git.js
+ */
+if(!$.ui) {
+ $['cleanData'+replaceSuffix] = $.cleanData;
+ $.cleanData = function( elems ) {
+ for(var i = 0, elem; (elem = $( elems[i] )).length && elem.attr(HASATTR); i++) {
+ try { elem.triggerHandler('removeqtip'); }
+ catch( e ) {}
+ }
+ $['cleanData'+replaceSuffix]( elems );
+ };
+}
+
+// Set global qTip properties
+QTIP.version = '2.0.1-35-';
+QTIP.nextid = 0;
+QTIP.inactiveEvents = 'click dblclick mousedown mouseup mousemove mouseleave mouseenter'.split(' ');
+QTIP.zindex = 15000;
+
+// Define configuration defaults
+QTIP.defaults = {
+ prerender: FALSE,
+ id: FALSE,
+ overwrite: TRUE,
+ suppress: TRUE,
+ content: {
+ text: TRUE,
+ attr: 'title',
+ deferred: FALSE,
+ title: {
+ text: FALSE,
+ button: FALSE
+ }
+ },
+ position: {
+ my: 'top left',
+ at: 'bottom right',
+ target: FALSE,
+ container: FALSE,
+ viewport: FALSE,
+ adjust: {
+ x: 0, y: 0,
+ mouse: TRUE,
+ scroll: TRUE,
+ resize: TRUE,
+ method: 'flipinvert flipinvert'
+ },
+ effect: function(api, pos, viewport) {
+ $(this).animate(pos, {
+ duration: 200,
+ queue: FALSE
+ });
+ }
+ },
+ show: {
+ target: FALSE,
+ event: 'mouseenter',
+ effect: TRUE,
+ delay: 90,
+ solo: FALSE,
+ ready: FALSE,
+ autofocus: FALSE
+ },
+ hide: {
+ target: FALSE,
+ event: 'mouseleave',
+ effect: TRUE,
+ delay: 0,
+ fixed: FALSE,
+ inactive: FALSE,
+ leave: 'window',
+ distance: FALSE
+ },
+ style: {
+ classes: '',
+ widget: FALSE,
+ width: FALSE,
+ height: FALSE,
+ def: TRUE
+ },
+ events: {
+ render: NULL,
+ move: NULL,
+ show: NULL,
+ hide: NULL,
+ toggle: NULL,
+ visible: NULL,
+ hidden: NULL,
+ focus: NULL,
+ blur: NULL
+ }
+};
+
+
+PLUGINS.svg = function(api, svg, corner, adjustMethod)
+{
+ var doc = $(document),
+ elem = svg[0],
+ result = {
+ width: 0, height: 0,
+ position: { top: 1e10, left: 1e10 }
+ },
+ box, mtx, root, point, tPoint;
+
+ // Ascend the parentNode chain until we find an element with getBBox()
+ while(!elem.getBBox) { elem = elem.parentNode; }
+
+ // Check for a valid bounding box method
+ if (elem.getBBox && elem.parentNode) {
+ box = elem.getBBox();
+ mtx = elem.getScreenCTM();
+ root = elem.farthestViewportElement || elem;
+
+ // Return if no method is found
+ if(!root.createSVGPoint) { return result; }
+
+ // Create our point var
+ point = root.createSVGPoint();
+
+ // Adjust top and left
+ point.x = box.x;
+ point.y = box.y;
+ tPoint = point.matrixTransform(mtx);
+ result.position.left = tPoint.x;
+ result.position.top = tPoint.y;
+
+ // Adjust width and height
+ point.x += box.width;
+ point.y += box.height;
+ tPoint = point.matrixTransform(mtx);
+ result.width = tPoint.x - result.position.left;
+ result.height = tPoint.y - result.position.top;
+
+ // Adjust by scroll offset
+ result.position.left += doc.scrollLeft();
+ result.position.top += doc.scrollTop();
+ }
+
+ return result;
+};
+
+
+var AJAX,
+ AJAXNS = '.qtip-ajax',
+ RSCRIPT = /