diff --git a/app/assets/images/marker-icon.png b/app/assets/images/marker-icon.png
new file mode 100644
index 000000000..de1f1568d
Binary files /dev/null and b/app/assets/images/marker-icon.png differ
diff --git a/app/assets/javascripts/address_typeahead.js b/app/assets/javascripts/address_typeahead.js
new file mode 100644
index 000000000..d0f8a434f
--- /dev/null
+++ b/app/assets/javascripts/address_typeahead.js
@@ -0,0 +1,22 @@
+function address_type_init() {
+ display = 'label';
+
+ var bloodhound = new Bloodhound({
+ datumTokenizer: Bloodhound.tokenizers.obj.whitespace(display),
+ queryTokenizer: Bloodhound.tokenizers.whitespace,
+
+ remote: {
+ url: '/ban/search?request=%QUERY',
+ wildcard: '%QUERY'
+ }
+ });
+ bloodhound.initialize();
+
+ $("input[type='address']").typeahead({
+ minLength: 1
+ }, {
+ display: display,
+ source: bloodhound,
+ limit: 5
+ });
+}
\ No newline at end of file
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 5ec3d6999..fa22bb903 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -29,8 +29,7 @@
//= require franceconnect
//= require bootstrap-wysihtml5
//= require bootstrap-wysihtml5/locales/fr-FR
-
-
+//= require typeahead.bundle
$(document).on('page:load', scroll_to);
$(document).ready(scroll_to);
diff --git a/app/assets/javascripts/carte/carte.js b/app/assets/javascripts/carte/carte.js
index 172d7a345..90ebef7b4 100644
--- a/app/assets/javascripts/carte/carte.js
+++ b/app/assets/javascripts/carte/carte.js
@@ -14,6 +14,17 @@ function initCarto() {
layers: [OSM]
});
+ icon = L.icon({
+ iconUrl: '/assets/marker-icon.png',
+ //shadowUrl: 'leaf-shadow.png',
+
+ iconSize: [34.48, 40], // size of the icon
+ //shadowSize: [50, 64], // size of the shadow
+ iconAnchor: [20, 20] // point of the icon which will correspond to marker's location
+ //shadowAnchor: [4, 62], // the same for the shadow
+ //popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor
+ });
+
if (qp_active())
display_qp(JSON.parse($("#quartier_prioritaires").val()));
@@ -39,6 +50,7 @@ function initCarto() {
map.setView(new L.LatLng(position.lat, position.lon), position.zoom);
add_event_freeDraw();
+ add_event_search_address();
}
function default_gestionnaire_position() {
@@ -111,8 +123,32 @@ function get_position() {
return position;
}
+function get_address_point(request) {
+ $.ajax({
+ url: '/ban/address_point?request=' + request,
+ dataType: 'json',
+ async: true
+ }).done(function (data) {
+ if (data.lat != null) {
+ map.setView(new L.LatLng(data.lat, data.lon), data.zoom);
+ L.marker([data.lat, data.lon], {icon: icon}).addTo(map);
+ }
+ });
+}
+
function jsObject_to_array(qp_list) {
return Object.keys(qp_list).map(function (v) {
return qp_list[v];
});
}
+
+function add_event_search_address() {
+ $("#search_by_address input[type='address']").bind('typeahead:select', function (ev, seggestion) {
+ get_address_point(seggestion['label']);
+ });
+
+ $("#search_by_address input[type='address']").keypress(function (e) {
+ if (e.which == 13)
+ get_address_point($(this).val());
+ });
+}
\ No newline at end of file
diff --git a/app/assets/javascripts/description.js b/app/assets/javascripts/description.js
index 7ca93b611..98d18a906 100644
--- a/app/assets/javascripts/description.js
+++ b/app/assets/javascripts/description.js
@@ -13,6 +13,16 @@ function action_type_de_champs() {
toggleErrorClass(this, validatePhone(val));
});
+
+ $("#liste_champs input").on('focus', function (){
+ $("#description_"+this.id).slideDown();
+ });
+
+ $("#liste_champs input").on('blur', function (){
+ $("#description_"+this.id).slideUp();
+ });
+
+ address_type_init();
}
function toggleErrorClass(node, boolean) {
@@ -34,4 +44,4 @@ function validateEmail(email) {
function validateInput(input, regex) {
return regex.test(input);
-}
\ No newline at end of file
+}
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index b5c193e7e..c28c00141 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -182,6 +182,10 @@ div.pagination {
text-align: center;
}
+.alert{
+ margin-bottom: 0px;
+}
+
.alert.alert-success.move_up {
position: absolute;
top: 0px;
diff --git a/app/assets/stylesheets/description.scss b/app/assets/stylesheets/description.scss
index 38947921c..e40c2d551 100644
--- a/app/assets/stylesheets/description.scss
+++ b/app/assets/stylesheets/description.scss
@@ -21,6 +21,15 @@
}
}
+.type_champ-address {
+ @extend .col-md-6;
+ @extend .col-lg-6;
+
+ input[type='address'] {
+ width: 100%;
+ }
+}
+
.type_champ-email {
@extend .col-md-4;
@extend .col-lg-4;
@@ -76,3 +85,10 @@
width: 100%;
}
}
+
+.description_div {
+ margin-top: 5px;
+ margin-left: 5px;
+ color: dimgrey;
+ display: none;
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/typeahead.scss b/app/assets/stylesheets/typeahead.scss
new file mode 100644
index 000000000..e29aff51f
--- /dev/null
+++ b/app/assets/stylesheets/typeahead.scss
@@ -0,0 +1,35 @@
+.tt-menu {
+ width: 555px;
+ padding: 8px 0;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ -webkit-border-radius: 8px;
+ -moz-border-radius: 8px;
+ border-radius: 8px;
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
+ box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
+}
+
+.tt-suggestion {
+ padding: 3px 20px;
+ font-size: 18px;
+ line-height: 24px;
+}
+
+.twitter-typeahead {
+ width: 555px;
+}
+
+.tt-suggestion:hover {
+ cursor: pointer;
+ color: #fff;
+ background-color: #0097cf;
+}
+
+.tt-suggestion.tt-cursor {
+ color: #fff;
+ background-color: #0097cf;
+
+}
\ No newline at end of file
diff --git a/app/controllers/admin/pieces_justificatives_controller.rb b/app/controllers/admin/pieces_justificatives_controller.rb
index 638596511..7ed5c7ef5 100644
--- a/app/controllers/admin/pieces_justificatives_controller.rb
+++ b/app/controllers/admin/pieces_justificatives_controller.rb
@@ -22,6 +22,23 @@ class Admin::PiecesJustificativesController < AdminController
def update_params
params
.require(:procedure)
- .permit(types_de_piece_justificative_attributes: [:libelle, :description, :id])
+ .permit(types_de_piece_justificative_attributes: [:libelle, :description, :id, :order_place])
+ end
+
+ def move_up
+ index = params[:index].to_i - 1
+ if @procedure.switch_types_de_piece_justificative index
+ render 'show', format: :js
+ else
+ render json: {}, status: 400
+ end
+ end
+
+ def move_down
+ if @procedure.switch_types_de_piece_justificative params[:index].to_i
+ render 'show', format: :js
+ else
+ render json: {}, status: 400
+ end
end
end
\ No newline at end of file
diff --git a/app/controllers/admin/procedures_controller.rb b/app/controllers/admin/procedures_controller.rb
index 8ee4befa2..bf5a137e3 100644
--- a/app/controllers/admin/procedures_controller.rb
+++ b/app/controllers/admin/procedures_controller.rb
@@ -7,7 +7,7 @@ class Admin::ProceduresController < AdminController
def index
@procedures = smart_listing_create :procedures,
- current_administrateur.procedures.where(archived: false),
+ current_administrateur.procedures.where(published: true, archived: false),
partial: "admin/procedures/list",
array: true
@@ -25,6 +25,18 @@ class Admin::ProceduresController < AdminController
render 'index'
end
+ def draft
+ @procedures = smart_listing_create :procedures,
+ current_administrateur.procedures.where(published: false, archived: false),
+ partial: "admin/procedures/draft_list",
+ array: true
+
+ draft_class
+
+ render 'index'
+ end
+
+
def show
@facade = AdminProceduresShowFacades.new @procedure.decorate
end
@@ -63,16 +75,12 @@ class Admin::ProceduresController < AdminController
redirect_to edit_admin_procedure_path(id: @procedure.id)
end
+ def publish
+ change_status({published: params[:published]})
+ end
+
def archive
- @procedure = current_administrateur.procedures.find(params[:procedure_id])
- @procedure.update_attributes({archived: params[:archive]})
-
- flash.notice = 'Procédure éditée'
- redirect_to admin_procedures_path
-
- rescue ActiveRecord::RecordNotFound
- flash.alert = 'Procédure inéxistante'
- redirect_to admin_procedures_path
+ change_status({archived: params[:archive]})
end
def active_class
@@ -83,6 +91,10 @@ class Admin::ProceduresController < AdminController
@archived_class = 'active'
end
+ def draft_class
+ @draft_class = 'active'
+ end
+
private
def create_procedure_params
@@ -92,4 +104,16 @@ class Admin::ProceduresController < AdminController
def create_module_api_carto_params
params.require(:procedure).require(:module_api_carto_attributes).permit(:id, :use_api_carto, :quartiers_prioritaires, :cadastre)
end
+
+ def change_status(status_options)
+ @procedure = current_administrateur.procedures.find(params[:procedure_id])
+ @procedure.update_attributes(status_options)
+
+ flash.notice = 'Procédure éditée'
+ redirect_to admin_procedures_path
+
+ rescue ActiveRecord::RecordNotFound
+ flash.alert = 'Procédure inéxistante'
+ redirect_to admin_procedures_path
+ end
end
diff --git a/app/controllers/admin/types_de_champ_controller.rb b/app/controllers/admin/types_de_champ_controller.rb
index b7af379ec..6aa974d74 100644
--- a/app/controllers/admin/types_de_champ_controller.rb
+++ b/app/controllers/admin/types_de_champ_controller.rb
@@ -1,7 +1,7 @@
class Admin::TypesDeChampController < AdminController
before_action :retrieve_procedure
before_action :procedure_locked?
-
+
def destroy
@procedure.types_de_champ.destroy(params[:id])
render 'show', format: :js
@@ -19,7 +19,9 @@ class Admin::TypesDeChampController < AdminController
end
def update_params
- params.require(:procedure).permit(types_de_champ_attributes: [:libelle, :description, :order_place, :type_champ, :id, :mandatory])
+ params
+ .require(:procedure)
+ .permit(types_de_champ_attributes: [:libelle, :description, :order_place, :type_champ, :id, :mandatory])
end
def move_up
diff --git a/app/controllers/ban/search_controller.rb b/app/controllers/ban/search_controller.rb
new file mode 100644
index 000000000..55780de7b
--- /dev/null
+++ b/app/controllers/ban/search_controller.rb
@@ -0,0 +1,20 @@
+class Ban::SearchController < ApplicationController
+ def get
+ request = params[:request]
+
+ render json: Carto::Bano::AddressRetriever.new(request).list.inject([]) {
+ |acc, value| acc.push({label: value})
+ }.to_json
+ end
+
+ def get_address_point
+ point = Carto::Geocodeur.convert_adresse_to_point(params[:request])
+
+ unless point.nil?
+ lon = point.x.to_s
+ lat = point.y.to_s
+ end
+
+ render json: {lon: lon, lat: lat, zoom: '14', dossier_id: params[:dossier_id]}
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb
index 3faaad661..a6aa36f39 100644
--- a/app/controllers/users/dossiers_controller.rb
+++ b/app/controllers/users/dossiers_controller.rb
@@ -23,7 +23,7 @@ class Users::DossiersController < UsersController
end
def new
- procedure = Procedure.where(archived: false).find(params[:procedure_id])
+ procedure = Procedure.where(archived: false, published: true).find(params[:procedure_id])
@dossier = Dossier.new(procedure: procedure)
@siret = params[:siret] || current_user.siret
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
index 40c598220..c69384199 100644
--- a/app/controllers/users/sessions_controller.rb
+++ b/app/controllers/users/sessions_controller.rb
@@ -12,7 +12,7 @@ class Users::SessionsController < Sessions::SessionsController
# GET /resource/sign_in
def new
unless user_return_to_procedure_id.nil?
- @dossier = Dossier.new(procedure: Procedure.not_archived(user_return_to_procedure_id))
+ @dossier = Dossier.new(procedure: Procedure.active(user_return_to_procedure_id))
end
@user = User.new
diff --git a/app/decorators/type_de_piece_justificative_decorator.rb b/app/decorators/type_de_piece_justificative_decorator.rb
new file mode 100644
index 000000000..284c1b854
--- /dev/null
+++ b/app/decorators/type_de_piece_justificative_decorator.rb
@@ -0,0 +1,37 @@
+
+class TypeDePieceJustificativeDecorator < Draper::Decorator
+ delegate_all
+ def button_up params
+ h.link_to '', params[:url], class: up_classes, id: "btn_up_#{params[:index]}", remote: true, method: :post if display_up_button?(params[:index])
+ end
+
+ def button_down params
+ h.link_to '', params[:url], class: down_classes, id: "btn_down_#{params[:index]}", remote: true, method: :post if display_down_button?(params[:index])
+ end
+
+ private
+
+ def up_classes
+ base_classes << 'fa-chevron-up'
+ end
+
+ def down_classes
+ base_classes << 'fa-chevron-down'
+ end
+
+ def base_classes
+ %w(btn btn-default form-control fa)
+ end
+
+ def display_up_button?(index)
+ !(index == 0 || count_type_de_piece_justificative < 2)
+ end
+
+ def display_down_button?(index)
+ (index + 1) < count_type_de_piece_justificative
+ end
+
+ def count_type_de_piece_justificative
+ @count_type_de_piece_justificative ||= procedure.types_de_piece_justificative.count
+ end
+end
\ No newline at end of file
diff --git a/app/models/champ.rb b/app/models/champ.rb
index 2679d1923..593acfe97 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -2,9 +2,14 @@ class Champ < ActiveRecord::Base
belongs_to :dossier
belongs_to :type_de_champ
- delegate :libelle, :type_champ, :order_place, :mandatory, to: :type_de_champ
+ delegate :libelle, :type_champ, :order_place, :mandatory, :description, to: :type_de_champ
def mandatory?
mandatory
end
+
+ def data_provide
+ return 'datepicker' if type_champ == 'datetime'
+ return 'typeahead' if type_champ == 'address'
+ end
end
diff --git a/app/models/piece_justificative.rb b/app/models/piece_justificative.rb
index 4d80291b1..1208069d2 100644
--- a/app/models/piece_justificative.rb
+++ b/app/models/piece_justificative.rb
@@ -5,7 +5,7 @@ class PieceJustificative < ActiveRecord::Base
belongs_to :user
- delegate :api_entreprise, :libelle, to: :type_de_piece_justificative
+ delegate :api_entreprise, :libelle, :order_place, to: :type_de_piece_justificative
alias_attribute :type, :type_de_piece_justificative_id
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 41aee684c..00aa073b6 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -25,22 +25,37 @@ class Procedure < ActiveRecord::Base
types_de_champ.order(:order_place)
end
+ def types_de_piece_justificative_ordered
+ types_de_piece_justificative.order(:order_place)
+ end
+
def self.not_archived id
Procedure.where(archived: false).find(id)
end
+ def self.active id
+ Procedure.where(archived: false, published: true).find(id)
+ end
+
def switch_types_de_champ index_of_first_element
+ switch_list_order(types_de_champ_ordered, index_of_first_element)
+ end
+
+ def switch_types_de_piece_justificative index_of_first_element
+ switch_list_order(types_de_piece_justificative_ordered, index_of_first_element)
+ end
+
+ def switch_list_order(list, index_of_first_element)
return false if index_of_first_element < 0
- types_de_champ_tmp = types_de_champ_ordered
- nb_types_de_champ = types_de_champ_tmp.count
- return false if index_of_first_element == nb_types_de_champ - 1
- return false if types_de_champ_ordered.count < 1
- types_de_champ_tmp[index_of_first_element].update_attributes(order_place: index_of_first_element + 1)
- types_de_champ_tmp[index_of_first_element + 1].update_attributes(order_place: index_of_first_element)
+ return false if index_of_first_element == list.count - 1
+ return false if list.count < 1
+ list[index_of_first_element].update_attributes(order_place: index_of_first_element + 1)
+ list[index_of_first_element + 1].update_attributes(order_place: index_of_first_element)
true
end
def locked?
- dossiers.where.not(state: :draft).count > 0
+ published?
end
+
end
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index aa5d4b87e..98b148447 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -1,12 +1,14 @@
class TypeDeChamp < ActiveRecord::Base
- enum type_champs: {text: 'text',
- email: 'email',
- phone: 'phone',
- civilite: 'civilite',
- textarea: 'textarea',
- datetime: 'datetime',
- number: 'number',
- checkbox: 'checkbox'
+ enum type_champs: {
+ text: 'text',
+ textarea: 'textarea',
+ datetime: 'datetime',
+ number: 'number',
+ checkbox: 'checkbox',
+ civilite: 'civilite',
+ email: 'email',
+ phone: 'phone',
+ address: 'address'
}
belongs_to :procedure
diff --git a/app/serializers/type_de_piece_justificative_serializer.rb b/app/serializers/type_de_piece_justificative_serializer.rb
index 57aae2903..9dc1743b0 100644
--- a/app/serializers/type_de_piece_justificative_serializer.rb
+++ b/app/serializers/type_de_piece_justificative_serializer.rb
@@ -1,6 +1,6 @@
class TypeDePieceJustificativeSerializer < ActiveModel::Serializer
attributes :id,
:libelle,
- :description
-
+ :description,
+ :order_place
end
\ No newline at end of file
diff --git a/app/views/admin/pieces_justificatives/_fields.html.haml b/app/views/admin/pieces_justificatives/_fields.html.haml
index 7149f7687..aaa552edd 100644
--- a/app/views/admin/pieces_justificatives/_fields.html.haml
+++ b/app/views/admin/pieces_justificatives/_fields.html.haml
@@ -1,14 +1,21 @@
-
-= f.fields_for :types_de_piece_justificative, types_de_piece_justificative do |ff|
+= f.fields_for :types_de_piece_justificative, types_de_piece_justificative, remote: true do |ff|
.form-inline
.form-group
%h4 Libellé
=ff.text_field :libelle, class: 'form-control libelle', placeholder: 'Libellé'
-
.form-group
%h4 Description
=ff.text_area :description, class: 'form-control description', placeholder: 'Description'
+ .form-group
+ = ff.hidden_field :order_place, value: ff.index
+ = ff.hidden_field :id
+ - unless ff.object.id.nil?
+ .form-group
+ %br
+ = ff.object.button_up(index: ff.index, url: move_up_admin_procedure_pieces_justificatives_path(@procedure, ff.index))
+ = ff.object.button_down(index: ff.index, url: move_down_admin_procedure_pieces_justificatives_path(@procedure, ff.index))
+
.form-group
%br
diff --git a/app/views/admin/pieces_justificatives/_form.html.haml b/app/views/admin/pieces_justificatives/_form.html.haml
index 0a9ee481a..7891ae363 100644
--- a/app/views/admin/pieces_justificatives/_form.html.haml
+++ b/app/views/admin/pieces_justificatives/_form.html.haml
@@ -1,8 +1,7 @@
= form_for [:admin, @procedure], url: admin_procedure_pieces_justificatives_path(@procedure) , remote: true do |f|
#liste_piece_justificative
- = render partial: 'fields', locals:{ types_de_piece_justificative: @procedure.types_de_piece_justificative, f: f }
+ = render partial: 'fields', locals:{ types_de_piece_justificative: @procedure.types_de_piece_justificative_ordered.decorate, f: f }
= f.submit "Enregistrer", class: 'btn btn-success', id: :save
%hr
-
#new_type_de_piece_justificative
- = render partial: 'fields', locals:{ types_de_piece_justificative: TypeDePieceJustificative.new, f: f }
+ = render partial: 'fields', locals:{ types_de_piece_justificative: TypeDePieceJustificative.new.decorate, f: f }
diff --git a/app/views/admin/pieces_justificatives/show.js.erb b/app/views/admin/pieces_justificatives/show.js.erb
index c41307172..094e982cb 100644
--- a/app/views/admin/pieces_justificatives/show.js.erb
+++ b/app/views/admin/pieces_justificatives/show.js.erb
@@ -1,4 +1,4 @@
<% flash.each do |type, message| %>
$("#flash_message").html("
<%= message.html_safe %>
").children().fadeOut(5000)
<% end %>
-$('#piece_justificative_form').html("<%= escape_javascript(render partial: 'form', locals: { procedure: @procedure } ) %>");
\ No newline at end of file
+$('#piece_justificative_form').html("<%= escape_javascript(render partial: 'form', locals: { procedure: @procedure } ) %>");
diff --git a/app/views/admin/procedures/_draft_list.html.haml b/app/views/admin/procedures/_draft_list.html.haml
new file mode 100644
index 000000000..7d3505614
--- /dev/null
+++ b/app/views/admin/procedures/_draft_list.html.haml
@@ -0,0 +1,19 @@
+- unless smart_listing.empty?
+ %table.table
+ %thead
+ %th#ID= smart_listing.sortable 'ID', 'id'
+ %th#libelle= smart_listing.sortable 'Libellé', 'libelle'
+
+ - @procedures.each do |procedure|
+ - procedure = procedure.decorate
+ %tr
+ %td.col-md-1.col-lg-1= procedure.id
+ %td.col-md-6.col-lg-6
+ = link_to(procedure.libelle, "/admin/procedures/#{procedure.id}")
+
+ = smart_listing.paginate
+ = smart_listing.pagination_per_page_links
+
+- else
+ %h4.center
+ Aucune procédure
diff --git a/app/views/admin/procedures/_onglets.html.haml b/app/views/admin/procedures/_onglets.html.haml
index 80fd9d40a..5c8e0a8c0 100644
--- a/app/views/admin/procedures/_onglets.html.haml
+++ b/app/views/admin/procedures/_onglets.html.haml
@@ -1,5 +1,10 @@
#onglets
%ul.nav.nav-tabs
+ %li{class: @draft_class}
+ %a{:href => "#{url_for :admin_procedures_draft}"}
+ %h5{style: 'color: black'}
+ ="Brouillons"
+
%li{class: @active_class}
%a{:href => "#{url_for :admin_procedures}"}
%h5.text-success
diff --git a/app/views/admin/procedures/show.html.haml b/app/views/admin/procedures/show.html.haml
index 6bb99288b..8626a87e2 100644
--- a/app/views/admin/procedures/show.html.haml
+++ b/app/views/admin/procedures/show.html.haml
@@ -1,30 +1,48 @@
#procedure_show
=render partial: 'head', locals: {active: 'Informations'}
- = form_tag admin_procedure_archive_path(procedure_id: @facade.procedure.id, archive: !@facade.procedure.archived?), method: :put, style:'float: right; margin-top: 10px' do
- %button#archive.btn.btn-small.btn-default.text-info{type: :button}
- %i.fa.fa-eraser
- - if @facade.procedure.archived
- = 'Réactiver'
- - else
- = 'Archiver'
- #confirm
- %button#valid.btn.btn-small.btn-success{type: :submit}
- %i.fa.fa-check
- Valider
- %button#cancel.btn.btn-small.btn-danger{type: :button}
- %i.fa.fa-remove
- Annuler
+ -if ! @facade.procedure.published?
+ = form_tag admin_procedure_publish_path(procedure_id: @facade.procedure.id, publish: true), method: :put, style:'float: right; margin-top: 10px' do
+ %button#archive.btn.btn-small.btn-success.text-info{type: :button}
+ %i.fa.fa-eraser
+ Publier
+ #confirm
+ %button#valid.btn.btn-small.btn-success{type: :submit}
+ %i.fa.fa-check
+ Valider
+ %button#cancel.btn.btn-small.btn-danger{type: :button}
+ %i.fa.fa-remove
+ Annuler
+
+ -else
+ = form_tag admin_procedure_archive_path(procedure_id: @facade.procedure.id, archive: !@facade.procedure.archived?), method: :put, style:'float: right; margin-top: 10px' do
+ %button#archive.btn.btn-small.btn-default.text-info{type: :button}
+ %i.fa.fa-eraser
+ - if @facade.procedure.archived
+ = 'Réactiver'
+ - else
+ = 'Archiver'
+ #confirm
+ %button#valid.btn.btn-small.btn-success{type: :submit}
+ %i.fa.fa-check
+ Valider
+ %button#cancel.btn.btn-small.btn-danger{type: :button}
+ %i.fa.fa-remove
+ Annuler
- if @facade.procedure.locked?
#procedure_locked.center
%h5
- .label.label-info La procédure ne peut plus être modifiée car un usagé a déjà déposé un dossier
+ .label.label-info La procédure ne peut plus être modifiée car elle a été publiée
%div
%h3 Lien procédure
%div{style:'margin-left:3%'}
- = @facade.procedure.lien
+ -if @facade.procedure.published?
+ = @facade.procedure.lien
+ -else
+ %b
+ Cette procédure n'a pas encore été publiée et n'est donc pas accessible par le public.
%br
%h3 Détails
diff --git a/app/views/layouts/_navbar.html.haml b/app/views/layouts/_navbar.html.haml
new file mode 100644
index 000000000..6ed7c1bc6
--- /dev/null
+++ b/app/views/layouts/_navbar.html.haml
@@ -0,0 +1,33 @@
+#beta
+ Beta
+= image_tag('marianne_small.png', class: 'logo')
+%a{href: '/'}
+ = image_tag('logo-tps.png', class: 'logo')
+%a{href: '/', class: 'btn btn-md'}
+ -if gestionnaire_signed_in? || user_signed_in?
+ Mes Dossiers
+ -elsif administrateur_signed_in?
+ Mes Procédures
+#sign_out
+ -if gestionnaire_signed_in?
+ = render partial: 'gestionnaires/login_banner'
+ -elsif administrateur_signed_in?
+ = render partial: 'administrateurs/login_banner'
+ - elsif user_signed_in?
+ %div.user
+ -if current_user.loged_in_with_france_connect?
+ %div{ id: "fconnect-profile", "data-fc-logout-url" => '/users/sign_out" data-method="delete' }
+ %a.text-info{ href: "#" }
+ = "#{current_user.given_name} #{current_user.family_name}"
+
+ = link_to "", '/users/sign_out', method: :delete, :class => 'btn fa fa-power-off off-fc-link'
+
+ -else
+ %i.fa.fa-user
+ = current_user.email
+
+ = link_to "Déconnexion", '/users/sign_out', method: :delete, :class => 'btn btn-md'
+ - else
+ = link_to "Utilisateur", '/users/sign_in', method: :get, :class => 'btn btn-md'
+ = link_to "Accompagnateur", '/gestionnaires/sign_in', method: :get, :class => 'btn btn-md'
+ = link_to "Administrateur", '/administrateurs/sign_in', method: :get, :class => 'btn btn-md'
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 4618fd213..440c59544 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -12,36 +12,7 @@
%body
%div#wrap
%div#header.navbar
-
- #beta
- Beta
- = image_tag('marianne_small.png', class: 'logo')
- %a{href: '/'}
- = image_tag('logo-tps.png', class: 'logo')
-
- #sign_out
- -if gestionnaire_signed_in?
- = render partial: 'gestionnaires/login_banner'
- -elsif administrateur_signed_in?
- = render partial: 'administrateurs/login_banner'
- - elsif user_signed_in?
- %div.user
- -if current_user.loged_in_with_france_connect?
- %div{ id: "fconnect-profile", "data-fc-logout-url" => '/users/sign_out" data-method="delete' }
- %a.text-info{ href: "#" }
- = "#{current_user.given_name} #{current_user.family_name}"
-
- = link_to "", '/users/sign_out', method: :delete, :class => 'btn fa fa-power-off off-fc-link'
-
- -else
- %i.fa.fa-user
- = current_user.email
-
- = link_to "Déconnexion", '/users/sign_out', method: :delete, :class => 'btn btn-md'
- - else
- = link_to "Utilisateur", '/users/sign_in', method: :get, :class => 'btn btn-md'
- = link_to "Accompagnateur", '/gestionnaires/sign_in', method: :get, :class => 'btn btn-md'
- = link_to "Administrateur", '/administrateurs/sign_in', method: :get, :class => 'btn btn-md'
+ =render partial: "layouts/navbar"
#flash_message.center
- if flash.notice
@@ -50,7 +21,6 @@
- if flash.alert
.alert.alert-danger
= flash.alert
-
#main_div.main_div
= yield
diff --git a/app/views/root/landing.html.haml b/app/views/root/landing.html.haml
index fdcbfbdba..700c56814 100644
--- a/app/views/root/landing.html.haml
+++ b/app/views/root/landing.html.haml
@@ -58,10 +58,14 @@
.row.word.news
.latest_release.col-md-7.col-lg-7
- %h3.text-info
- = "Dernière version (#{@latest_release.tag_name} - #{@latest_release.published_at})"
- .body
- =@latest_release.body.html_safe
+ - if @latest_release.nil?
+ %p
+ Erreur dans la récupération des données
+ -else
+ %h3.text-info
+ = "Dernière version (#{@latest_release.tag_name} - #{@latest_release.published_at})"
+ .body
+ =@latest_release.body.html_safe
.center
\-
diff --git a/app/views/users/carte/show.html.haml b/app/views/users/carte/show.html.haml
index 6e310a059..75e0f6311 100644
--- a/app/views/users/carte/show.html.haml
+++ b/app/views/users/carte/show.html.haml
@@ -9,6 +9,8 @@
\-
%button#delete.btn.btn-sm.btn-danger{type:'button'} Supprimer
+ %span#search_by_address{style: 'margin-left: 20px'}
+ %input.form-control{type: :address, placeholder: 'Rechercher une adresse'}
%br
%br
#carte_page.row
@@ -24,7 +26,6 @@
%h3.text-warning Cadastres
%ul
-
= form_tag(url_for({controller: :carte, action: :save, dossier_id: @dossier.id}), class: 'form-inline', method: 'POST') do
%br
%input{type: 'hidden', value: "#{@dossier.json_latlngs}", name: 'json_latlngs', id: 'json_latlngs'}
diff --git a/app/views/users/description/_champs.html.haml b/app/views/users/description/_champs.html.haml
index d1a724967..464572686 100644
--- a/app/views/users/description/_champs.html.haml
+++ b/app/views/users/description/_champs.html.haml
@@ -34,5 +34,9 @@
id: "champs_#{champ.id}",
value: champ.value,
type: champ.type_champ,
- 'data-provide' => ('datepicker' if champ.type_champ == 'datetime'),
- 'data-date-format' => ('dd/mm/yyyy' if champ.type_champ == 'datetime')}
\ No newline at end of file
+ 'data-provide' => champ.data_provide,
+ 'data-date-format' => ('dd/mm/yyyy' if champ.type_champ == 'datetime')}
+ - unless champ.description.empty?
+ .row
+ .col-lg-8.col-md-8{class: 'description_div', id:"description_champs_#{champ.id}"}
+ = champ.description
diff --git a/config/routes.rb b/config/routes.rb
index 03c1c4a11..3b5f6ebb2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -82,6 +82,7 @@ Rails.application.routes.draw do
namespace :admin do
get 'sign_in' => '/administrateurs/sessions#new'
get 'procedures/archived' => 'procedures#archived'
+ get 'procedures/draft' => 'procedures#draft'
get 'profile' => 'profile#show', as: :profile
resources :procedures do
@@ -89,8 +90,13 @@ Rails.application.routes.draw do
post '/:index/move_up' => 'types_de_champ#move_up', as: :move_up
post '/:index/move_down' => 'types_de_champ#move_down', as: :move_down
end
+ resource :pieces_justificatives, only: [:show, :update] do
+ post '/:index/move_up' => 'pieces_justificatives#move_up', as: :move_up
+ post '/:index/move_down' => 'pieces_justificatives#move_down', as: :move_down
+ end
put 'archive' => 'procedures#archive', as: :archive
+ put 'publish' => 'procedures#publish', as: :publish
resource :accompagnateurs, only: [:show, :update]
@@ -108,6 +114,11 @@ Rails.application.routes.draw do
resources :gestionnaires, only: [:index, :create, :destroy]
end
+ namespace :ban do
+ get 'search' => 'search#get'
+ get 'address_point' => 'search#get_address_point'
+ end
+
get 'backoffice' => 'backoffice#index'
namespace :backoffice do
diff --git a/db/migrate/20160607150440_add_order_place_in_type_de_piece_justificative.rb b/db/migrate/20160607150440_add_order_place_in_type_de_piece_justificative.rb
new file mode 100644
index 000000000..7cf5fa801
--- /dev/null
+++ b/db/migrate/20160607150440_add_order_place_in_type_de_piece_justificative.rb
@@ -0,0 +1,9 @@
+class AddOrderPlaceInTypeDePieceJustificative < ActiveRecord::Migration
+ def up
+ add_column :types_de_piece_justificative, :order_place, :integer
+ end
+
+ def down
+ remove_column :types_de_piece_justificative, :order_place
+ end
+end
diff --git a/db/migrate/20160609125949_add_procedure_status.rb b/db/migrate/20160609125949_add_procedure_status.rb
new file mode 100644
index 000000000..6d4ea7e2e
--- /dev/null
+++ b/db/migrate/20160609125949_add_procedure_status.rb
@@ -0,0 +1,12 @@
+class AddProcedureStatus < ActiveRecord::Migration
+ class Procedure < ActiveRecord::Base
+ end
+
+ def change
+ add_column :procedures, :published, :boolean, default: false, null: false
+ Procedure.all.each do |procedure|
+ procedure.published = true
+ procedure.save!
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1c61d1297..4839009ec 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160524093540) do
+ActiveRecord::Schema.define(version: 20160609125949) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -242,6 +242,7 @@ ActiveRecord::Schema.define(version: 20160524093540) do
t.string "logo"
t.boolean "cerfa_flag", default: false
t.string "logo_secure_token"
+ t.boolean "published", default: false, null: false
end
create_table "quartier_prioritaires", force: :cascade do |t|
@@ -278,6 +279,7 @@ ActiveRecord::Schema.define(version: 20160524093540) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "procedure_id"
+ t.integer "order_place"
end
create_table "users", force: :cascade do |t|
diff --git a/lib/carto/bano/address_retriever.rb b/lib/carto/bano/address_retriever.rb
new file mode 100644
index 000000000..83fb03de3
--- /dev/null
+++ b/lib/carto/bano/address_retriever.rb
@@ -0,0 +1,36 @@
+module Carto
+ module Bano
+ # input : address
+ # output : Array List label address
+ class AddressRetriever
+ def initialize(address)
+ @address = address
+ end
+
+ def list
+ @list ||= convert_driver_result_to_full_address
+ end
+
+ private
+
+ def driver
+ @driver ||= Carto::Bano::Driver.new @address, 5
+ end
+
+ def convert_driver_result_to_full_address
+ result = JSON.parse(driver.call)
+
+ if result['features'].size == 0
+ Rails.logger.error "unable to find location for address #{@address}"
+ return []
+ end
+
+ result['features'].inject([]) do |acc, feature|
+ acc.push feature['properties']['label']
+ end
+ rescue TypeError, JSON::ParserError
+ []
+ end
+ end
+ end
+end
diff --git a/lib/carto/bano/driver.rb b/lib/carto/bano/driver.rb
index 60ec57f27..5d18382fb 100644
--- a/lib/carto/bano/driver.rb
+++ b/lib/carto/bano/driver.rb
@@ -3,12 +3,15 @@ module Carto
# input : string (address)
# output : json
class Driver
- def initialize(address)
+ def initialize(address, limit = 1)
@address = address
+ @limit = limit
end
def call
- RestClient.get api_url, params: { q: @address, limit: 1 }
+ RestClient.get api_url, params: { q: @address, limit: @limit }
+ rescue RestClient::ServiceUnavailable
+ nil
end
def api_url
diff --git a/lib/github/api.rb b/lib/github/api.rb
index 57c815fb7..8aab0ce86 100644
--- a/lib/github/api.rb
+++ b/lib/github/api.rb
@@ -12,7 +12,9 @@ class Github::API
def self.call(end_point, params = {})
RestClient::Resource.new(
- base_uri+end_point
+ base_uri+end_point, timeout: 5
).get(params: params)
+ rescue RestClient::Forbidden
+ nil
end
end
diff --git a/lib/github/releases.rb b/lib/github/releases.rb
index f58f66b20..f71449b66 100644
--- a/lib/github/releases.rb
+++ b/lib/github/releases.rb
@@ -2,8 +2,10 @@ class Github::Releases
def self.latest
release = Hashie::Mash.new JSON.parse(Github::API.latest_release)
- release.published_at = release.published_at.to_date.strftime('%d/%m/%Y')
+ return nil if release.nil?
+
+ release.published_at = release.published_at.to_date.strftime('%d/%m/%Y')
release
end
end
\ No newline at end of file
diff --git a/spec/controllers/admin/pieces_justificatives_controller_spec.rb b/spec/controllers/admin/pieces_justificatives_controller_spec.rb
index 1c6ddca9c..5c33da67e 100644
--- a/spec/controllers/admin/pieces_justificatives_controller_spec.rb
+++ b/spec/controllers/admin/pieces_justificatives_controller_spec.rb
@@ -2,12 +2,13 @@ require 'spec_helper'
describe Admin::PiecesJustificativesController, type: :controller do
let(:admin) { create(:administrateur) }
+ let(:published) { false }
+ let(:procedure) { create(:procedure, administrateur: admin, published: published) }
before do
sign_in admin
end
describe 'GET #show' do
- let(:procedure) { create(:procedure, administrateur: admin) }
let(:procedure_id) { procedure.id }
subject { get :show, procedure_id: procedure_id }
@@ -17,8 +18,8 @@ describe Admin::PiecesJustificativesController, type: :controller do
it { expect(subject.status).to eq(404) }
end
- context 'when procedure have at least a file' do
- let!(:dossier) { create(:dossier, procedure: procedure, state: :initiated) }
+ context 'when procedure is published' do
+ let(:published) { true }
it { is_expected.to redirect_to admin_procedure_path id: procedure_id }
end
@@ -30,7 +31,6 @@ describe Admin::PiecesJustificativesController, type: :controller do
end
describe 'PUT #update' do
- let(:procedure) { create(:procedure, administrateur: admin) }
let(:procedure_id) { procedure.id }
let(:libelle) { 'RIB' }
let(:description) { "relevé d'identité bancaire" }
@@ -72,7 +72,6 @@ describe Admin::PiecesJustificativesController, type: :controller do
end
describe 'DELETE #destroy' do
- let(:procedure) { create(:procedure, administrateur: admin) }
let!(:pj) { create(:type_de_piece_justificative, procedure: procedure) }
let(:procedure_id) { procedure.id }
let(:pj_id) { pj.id }
@@ -97,4 +96,76 @@ describe Admin::PiecesJustificativesController, type: :controller do
it { expect{ subject }.to change(TypeDePieceJustificative, :count).by(-1) }
end
end
+
+ describe 'POST #move_up' do
+ subject { post :move_up, procedure_id: procedure.id, index: index, format: :js }
+
+ context 'when procedure have no type de champ' do
+ let(:index) { 0 }
+ it { expect(subject.status).to eq(400) }
+ end
+ context 'when procedure have only one type de champ' do
+ let(:index) { 1 }
+ let!(:type_de_piece_justificative) { create(:type_de_piece_justificative, procedure: procedure) }
+ it { expect(subject.status).to eq(400) }
+ end
+ context 'when procedure have tow type de champs' do
+ context 'when index == 0' do
+ let(:index) { 0 }
+ let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure) }
+ let!(:type_de_piece_justificative_2) { create(:type_de_piece_justificative, procedure: procedure) }
+ it { expect(subject.status).to eq(400) }
+ end
+ context 'when index > 0' do
+ let(:index) { 1 }
+ let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
+ let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
+
+ it { expect(subject.status).to eq(200) }
+ it { expect(subject).to render_template('show') }
+ it 'changes order places' do
+ post :move_up, procedure_id: procedure.id, index: index, format: :js
+ type_de_piece_justificative_0.reload
+ type_de_piece_justificative_1.reload
+ expect(type_de_piece_justificative_0.order_place).to eq(1)
+ expect(type_de_piece_justificative_1.order_place).to eq(0)
+ end
+ end
+ end
+ end
+
+ describe 'POST #move_down' do
+ let(:request) { post :move_down, procedure_id: procedure.id, index: index, format: :js }
+ let(:index) { 0 }
+
+ subject { request }
+
+ context 'when procedure have no type de champ' do
+ it { expect(subject.status).to eq(400) }
+ end
+ context 'when procedure have only one type de champ' do
+ let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure) }
+ it { expect(subject.status).to eq(400) }
+ end
+ context 'when procedure have 2 type de champ' do
+ let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
+ let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
+ context 'when index represent last type_de_piece_justificative' do
+ let(:index) { 1 }
+ it { expect(subject.status).to eq(400) }
+ end
+ context 'when index does not represent last type_de_piece_justificative' do
+ let(:index) { 0 }
+ it { expect(subject.status).to eq(200) }
+ it { expect(subject).to render_template('show') }
+ it 'changes order place' do
+ request
+ type_de_piece_justificative_0.reload
+ type_de_piece_justificative_1.reload
+ expect(type_de_piece_justificative_0.order_place).to eq(1)
+ expect(type_de_piece_justificative_1.order_place).to eq(0)
+ end
+ end
+ end
+ end
end
\ No newline at end of file
diff --git a/spec/controllers/admin/procedures_controller_spec.rb b/spec/controllers/admin/procedures_controller_spec.rb
index c4d414b0a..11587b94d 100644
--- a/spec/controllers/admin/procedures_controller_spec.rb
+++ b/spec/controllers/admin/procedures_controller_spec.rb
@@ -46,8 +46,15 @@ describe Admin::ProceduresController, type: :controller do
it { expect(response.status).to eq(200) }
end
+ describe 'GET #published' do
+ subject { get :published }
+
+ it { expect(response.status).to eq(200) }
+ end
+
describe 'GET #edit' do
- let(:procedure) { create(:procedure, administrateur: admin) }
+ let(:published) { false }
+ let(:procedure) { create(:procedure, administrateur: admin, published: published) }
let(:procedure_id) { procedure.id }
subject { get :edit, id: procedure_id }
@@ -66,8 +73,8 @@ describe Admin::ProceduresController, type: :controller do
it { expect(subject).to have_http_status(:success) }
end
- context 'when procedure have at least a file' do
- let!(:dossier) { create(:dossier, procedure: procedure, state: :initiated) }
+ context 'when procedure is published' do
+ let(:published) { true }
it { is_expected.to redirect_to admin_procedure_path id: procedure_id }
end
diff --git a/spec/controllers/admin/types_de_champ_controller_spec.rb b/spec/controllers/admin/types_de_champ_controller_spec.rb
index 5227c56ae..c334ccb0c 100644
--- a/spec/controllers/admin/types_de_champ_controller_spec.rb
+++ b/spec/controllers/admin/types_de_champ_controller_spec.rb
@@ -9,7 +9,8 @@ describe Admin::TypesDeChampController, type: :controller do
end
describe 'GET #show' do
- let(:procedure) { create(:procedure, administrateur: admin) }
+ let(:published) { false }
+ let(:procedure) { create(:procedure, administrateur: admin, published: published) }
let(:procedure_id) { procedure.id }
subject { get :show, procedure_id: procedure_id }
@@ -19,8 +20,8 @@ describe Admin::TypesDeChampController, type: :controller do
it { expect(subject.status).to eq(404) }
end
- context 'when procedure have at least a file' do
- let!(:dossier) { create(:dossier, procedure: procedure, state: :initiated) }
+ context 'when procedure is published' do
+ let(:published) { true }
it { is_expected.to redirect_to admin_procedure_path id: procedure_id }
end
diff --git a/spec/controllers/ban/search_controller_spec.rb b/spec/controllers/ban/search_controller_spec.rb
new file mode 100644
index 000000000..5d799817e
--- /dev/null
+++ b/spec/controllers/ban/search_controller_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Ban::SearchController, type: :controller do
+ describe '#GET' do
+
+ let (:request) { '' }
+
+ before do
+ stub_request(:get, "http://api-adresse.data.gouv.fr/search?limit=5&q=").
+ to_return(:status => 200, :body => 'Missing query', :headers => {})
+ get :get, request: request
+ end
+
+ it { expect(response.status).to eq 200 }
+ end
+end
diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb
index f996114da..637f39710 100644
--- a/spec/controllers/users/dossiers_controller_spec.rb
+++ b/spec/controllers/users/dossiers_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Users::DossiersController, type: :controller do
let(:user) { create(:user) }
- let(:procedure) { create(:procedure) }
+ let(:procedure) { create(:procedure, :published) }
let(:procedure_id) { procedure.id }
let(:dossier) { create(:dossier, :with_entreprise, user: user, procedure: procedure) }
let(:dossier_id) { dossier.id }
@@ -80,6 +80,16 @@ describe Users::DossiersController, type: :controller do
it { is_expected.to redirect_to users_dossiers_path }
end
+
+ context 'when procedure is not published' do
+ let(:procedure) { create(:procedure, published: false) }
+
+ before do
+ sign_in create(:user)
+ end
+
+ it { is_expected.to redirect_to users_dossiers_path }
+ end
end
end
diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb
index 256913b1f..9728aba35 100644
--- a/spec/controllers/users/sessions_controller_spec.rb
+++ b/spec/controllers/users/sessions_controller_spec.rb
@@ -85,8 +85,18 @@ describe Users::SessionsController, type: :controller do
it { expect(subject).to redirect_to root_path }
end
+ context 'when procedure is not published' do
+ let(:procedure) { create :procedure, published: false }
+ before do
+ session["user_return_to"] = "?procedure_id=#{procedure.id}"
+ end
+
+ it { expect(subject.status).to eq 302}
+ it { expect(subject).to redirect_to root_path }
+ end
+
context 'when procedure_id exist' do
- let(:procedure) { create :procedure }
+ let(:procedure) { create :procedure, published: true }
before do
session["user_return_to"] = "?procedure_id=#{procedure.id}"
diff --git a/spec/decorators/type_de_champ_decorator_spec.rb b/spec/decorators/type_de_champ_decorator_spec.rb
new file mode 100644
index 000000000..61a2bc73f
--- /dev/null
+++ b/spec/decorators/type_de_champ_decorator_spec.rb
@@ -0,0 +1,55 @@
+
+require 'spec_helper'
+
+describe TypeDeChampDecorator do
+ let(:procedure) { create(:procedure) }
+ let(:url) { 'http://localhost' }
+ let(:params) { { url: url, index: index } }
+ let!(:type_de_champ_0) { create(:type_de_champ, procedure: procedure, order_place: 0) }
+ let!(:type_de_champ_1) { create(:type_de_champ, procedure: procedure, order_place: 1) }
+ let!(:type_de_champ_2) { create(:type_de_champ, procedure: procedure, order_place: 2) }
+
+ describe '#button_up' do
+
+ describe 'with first piece justificative' do
+ let(:index) { 0 }
+ subject { type_de_champ_0.decorate }
+ let(:button_up) { type_de_champ_.decorate }
+
+ it 'returns a button up' do
+ expect(subject.button_up(params)).to be(nil)
+ end
+ it 'returns a button down' do
+ expect(subject.button_down(params)).to match(/fa-chevron-down/)
+ end
+ end
+
+ describe 'with second out of three piece justificative' do
+ let(:index) { 1 }
+ subject { type_de_champ_1.decorate }
+ let(:button_up) { type_de_champ_1.decorate }
+
+ it 'returns a button up' do
+ expect(subject.button_up(params)).to match(/fa-chevron-up/)
+ end
+ it 'returns a button down' do
+ expect(subject.button_down(params)).to match(/fa-chevron-down/)
+ end
+ end
+
+ describe 'with last piece justificative' do
+ let(:index) { 2 }
+ subject { type_de_champ_2.decorate }
+ let(:button_up) { type_de_champ_1.decorate }
+
+ it 'returns a button up' do
+ expect(subject.button_up(params)).to match(/fa-chevron-up/)
+ end
+ it 'returns a button down' do
+ expect(subject.button_down(params)).to be(nil)
+ end
+ end
+ end
+
+
+end
\ No newline at end of file
diff --git a/spec/decorators/type_de_piece_justificative_decorator_spec.rb b/spec/decorators/type_de_piece_justificative_decorator_spec.rb
new file mode 100644
index 000000000..45e0d401d
--- /dev/null
+++ b/spec/decorators/type_de_piece_justificative_decorator_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe TypeDePieceJustificativeDecorator do
+ let(:procedure) { create(:procedure) }
+ let(:url) { 'http://localhost' }
+ let(:params) { { url: url, index: index } }
+ let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
+ let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
+ let!(:type_de_piece_justificative_2) { create(:type_de_piece_justificative, procedure: procedure, order_place: 2) }
+
+ describe '#button_up' do
+
+ describe 'with first piece justificative' do
+ let(:index) { 0 }
+ subject { type_de_piece_justificative_0.decorate }
+ let(:button_up) { type_de_piece_justificative_.decorate }
+
+ it 'returns a button up' do
+ expect(subject.button_up(params)).to be(nil)
+ end
+ it 'returns a button down' do
+ expect(subject.button_down(params)).to match(/fa-chevron-down/)
+ end
+ end
+
+ describe 'with second out of three piece justificative' do
+ let(:index) { 1 }
+ subject { type_de_piece_justificative_1.decorate }
+ let(:button_up) { type_de_piece_justificative_1.decorate }
+
+ it 'returns a button up' do
+ expect(subject.button_up(params)).to match(/fa-chevron-up/)
+ end
+ it 'returns a button down' do
+ expect(subject.button_down(params)).to match(/fa-chevron-down/)
+ end
+ end
+
+ describe 'with last piece justificative' do
+ let(:index) { 2 }
+ subject { type_de_piece_justificative_2.decorate }
+ let(:button_up) { type_de_piece_justificative_1.decorate }
+
+ it 'returns a button up' do
+ expect(subject.button_up(params)).to match(/fa-chevron-up/)
+ end
+ it 'returns a button down' do
+ expect(subject.button_down(params)).to be(nil)
+ end
+ end
+ end
+
+
+end
\ No newline at end of file
diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb
new file mode 100644
index 000000000..96620cb80
--- /dev/null
+++ b/spec/factories/champ.rb
@@ -0,0 +1,4 @@
+FactoryGirl.define do
+ factory :champ do
+ end
+end
diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb
index c8dea5a80..2614d06e6 100644
--- a/spec/factories/procedure.rb
+++ b/spec/factories/procedure.rb
@@ -5,6 +5,7 @@ FactoryGirl.define do
description "Demande de subvention à l'intention des associations"
organisation "Orga SGMAP"
direction "direction SGMAP"
+ published false
after(:build) do |procedure, _evaluator|
if procedure.module_api_carto.nil?
@@ -44,5 +45,11 @@ FactoryGirl.define do
procedure.types_de_piece_justificative << msa
end
end
+
+ trait :published do
+ after(:build) do |procedure, _evaluator|
+ procedure.published = true
+ end
+ end
end
end
diff --git a/spec/features/admin/procedure_locked_spec.rb b/spec/features/admin/procedure_locked_spec.rb
index 3dea1b3b0..4676e9043 100644
--- a/spec/features/admin/procedure_locked_spec.rb
+++ b/spec/features/admin/procedure_locked_spec.rb
@@ -3,26 +3,27 @@ require 'spec_helper'
feature 'procedure locked' do
let(:administrateur) { create(:administrateur) }
- let(:procedure) { create(:procedure, administrateur: administrateur) }
+ let(:published) { false }
+ let(:procedure) { create(:procedure, administrateur: administrateur, published: published) }
before do
login_as administrateur, scope: :administrateur
visit admin_procedure_path(procedure)
end
- context 'when procedure have no file' do
+ context 'when procedure is not published' do
scenario 'info label is not present' do
- expect(page).not_to have_content('La procédure ne peut plus être modifiée car un usagé a déjà déposé un dossier')
+ expect(page).not_to have_content('La procédure ne peut plus être modifiée car elle a été publiée')
end
end
- context 'when procedure have at least a file' do
+ context 'when procedure is published' do
+ let(:published) { true }
before do
- create(:dossier, procedure: procedure, state: :initiated)
visit admin_procedure_path(procedure)
end
scenario 'info label is present' do
- expect(page).to have_content('La procédure ne peut plus être modifiée car un usagé a déjà déposé un dossier')
+ expect(page).to have_content('La procédure ne peut plus être modifiée car elle a été publiée')
end
context 'when user click on Description tab' do
@@ -45,7 +46,7 @@ feature 'procedure locked' do
end
end
- context 'when user click on Pieces Justificatiives tab' do
+ context 'when user click on Pieces Justificatives tab' do
before do
page.click_on 'Pièces justificatives'
end
diff --git a/spec/features/users/complete_demande_spec.rb b/spec/features/users/complete_demande_spec.rb
index 46b766c06..a1f8dc0f8 100644
--- a/spec/features/users/complete_demande_spec.rb
+++ b/spec/features/users/complete_demande_spec.rb
@@ -2,9 +2,10 @@ require 'spec_helper'
feature 'user path for dossier creation' do
let(:user) { create(:user) }
- let(:procedure) { create(:procedure) }
+ let(:procedure) { create(:procedure, :published) }
let(:siret) { '53272417600013' }
let(:siren) { siret[0...9] }
+
context 'user arrives on siret page' do
before do
visit new_users_dossiers_path(procedure_id: procedure.id)
@@ -65,4 +66,15 @@ feature 'user path for dossier creation' do
end
end
end
+
+ context 'user cannot access non-published procedures' do
+ let(:procedure) { create(:procedure) }
+ before do
+ visit new_users_dossiers_path(procedure_id: procedure.id)
+ end
+
+ scenario 'user is on home page', vcr: { cassette_name: 'complete_demande_spec' } do
+ expect(page).to have_content('La procédure n\'existe pas')
+ end
+ end
end
\ No newline at end of file
diff --git a/spec/features/users/start_demande_spec.rb b/spec/features/users/start_demande_spec.rb
index 135b7f499..480550df1 100644
--- a/spec/features/users/start_demande_spec.rb
+++ b/spec/features/users/start_demande_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'user arrive on siret page' do
- let(:procedure) { create(:procedure) }
+ let(:procedure) { create(:procedure, :published) }
let(:user) { create(:user) }
let(:siret) { '42149333900020' }
let(:siren) { siret[0...9] }
diff --git a/spec/fixtures/cassettes/complete_demande_spec.yml b/spec/fixtures/cassettes/complete_demande_spec.yml
new file mode 100644
index 000000000..95841bbea
--- /dev/null
+++ b/spec/fixtures/cassettes/complete_demande_spec.yml
@@ -0,0 +1,101 @@
+---
+http_interactions:
+- request:
+ method: get
+ uri: https://api.github.com/repos/sgmap/tps/releases/latest
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Accept:
+ - "*/*; q=0.5, application/xml"
+ Accept-Encoding:
+ - gzip, deflate
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Server:
+ - GitHub.com
+ Date:
+ - Thu, 09 Jun 2016 14:42:08 GMT
+ Content-Type:
+ - application/json; charset=utf-8
+ Transfer-Encoding:
+ - chunked
+ Status:
+ - 200 OK
+ X-Ratelimit-Limit:
+ - '60'
+ X-Ratelimit-Remaining:
+ - '43'
+ X-Ratelimit-Reset:
+ - '1465485629'
+ Cache-Control:
+ - public, max-age=60, s-maxage=60
+ Vary:
+ - Accept
+ - Accept-Encoding
+ Etag:
+ - W/"0962b5ade3f87b4e0092d56f4719512e"
+ Last-Modified:
+ - Fri, 03 Jun 2016 10:05:19 GMT
+ X-Github-Media-Type:
+ - github.v3
+ Access-Control-Expose-Headers:
+ - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset,
+ X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
+ Access-Control-Allow-Origin:
+ - "*"
+ Content-Security-Policy:
+ - default-src 'none'
+ Strict-Transport-Security:
+ - max-age=31536000; includeSubdomains; preload
+ X-Content-Type-Options:
+ - nosniff
+ X-Frame-Options:
+ - deny
+ X-Xss-Protection:
+ - 1; mode=block
+ X-Served-By:
+ - c6c65e5196703428e7641f7d1e9bc353
+ Content-Encoding:
+ - gzip
+ X-Github-Request-Id:
+ - B918B84A:B215:2AB0D00D:5759803F
+ body:
+ encoding: ASCII-8BIT
+ string: !binary |-
+ H4sIAAAAAAAAA6VW7WobRxR9lWH/+I/jlSrjEuE2mEAhYEogTiltihjtXq0m
+ zM5s5mMjR/hdQn9Fz7Ev1nNHK1lSZAdRsJFXe++Ze88994yXWXQ6G2fzEBo/
+ znPZqItKhXmcXhS2zh011ue+qmWTIwDPmqQnn49GV1ejwc/ZeSa9p+An/wsm
+ X4MALTbayvIAbf2lP6WwHnH5ysiazrWckn4A/DzU+gB8p9sjfQZZ5e3wYoRc
+ VWbjvu3zDN9PGBrU9a+DdBWFCVirVVB+jje19IEcUncixVjczGZUBBmUNaIk
+ L2SBpEZWRgaKzgsKolHdtwKvPkYf1EwVCG7xWN4DSX2K5AFaOjkL2XgmtSeM
+ IYa5ddl4mWlbKYPT//wY9ZthX/hwNBhevrwaILCVKPVwXunLDcPRkyusCWRC
+ UkHMN+mv2l+Yisr1KMxJxmN7TkQM5/NtOc/PYBs2s1rbz8g8LHVfobvg+TYH
+ Ja3/VqY6OR85y9yGOYEllM66qZT/kcb3Cknxy5w/JqpkBA9+HR0q+2Dd9jD6
+ DJTy2aCKZVrGBBWnvnCqYQGdws5eHnCsq6RRX5IQT8FBHusvecMJ5KZ45FEL
+ XZ2SuE5Y5o1TrSzumQJHBWEjysnJYAeZwAr3Da/xe4yayVWBJrKseYXSaj2c
+ Z42j3vi261Y4wraWE4kNzH4aDK9eDPAzuhu8HA/xM/gLUE2cahjBkajhYDy4
+ HF+OOKq3vvHf/7CruKnUhw71A0/ukzY29UU1p2P0SRuMqS3v0df1fPTr7za2
+ BHPpVv46x/MH98FcR73+1Cp9vjdCGjiUchFuZqS+9yRqqdhB8EKQEYHqxgvX
+ rUiLYBGm2c5sEWsWg/gUFaKCw6/w0QktRaNB8My6mi7SmXl/2PXm0KDA7qNl
+ HrpoQygccHt1tPaeXDLdx7NLBU2gPg4uqYmKixMzHRd4FOzneCMRhCQ8zyX8
+ F+neK3Ln3Fuj0Q9jPuXaVEtTonVEWDTHbq+7Fa4GPqhHOt7kLXiQFYK4EGNo
+ wXdGAolGxEcGBPm9TpN7L0KUoAgHi+5rotTZoluV0ZHwShSko3pR9MQn2u/e
+ vhOV6/4tUgaO0Ar94XCzVsHzxbIMeHFgek7yVSYaintlyXTzoe+ETrsFHbsI
+ fdOtClx/fOEx0TNriv7WFPgnREzJW2X8hWCB3J7tK0AAvyXnWEmYDw9trbk0
+ uH5gW0Kg27hAjOYYpROf62q71cFo8o36sQ03dbfSyqJdDGa7H8d25Ba4OwNj
+ KXjffSsPJMqstCjVK2gErfPwtqIEo71a1hogs9HzekCp+OMiPC6vOxsDUp5O
+ Ex5C2l2hOYZrXRIVcwpdp0EmfeB4ZtyzRpm/VB43gH9osPObk/oQuAGC0tIp
+ 31ijplwGpqxVZZ7Y+T26MUJcAQ5C2PeHzXzSt5jRa4uLN+nm0cGOTegxjhWv
+ z2SJApORaZaCKuZpGNKAsDNaNNYF8frdH6I8w6Js5tIqibc3b9/wLh0nHWuy
+ u7m8BrwmSaXSFXPVdiuu4KgxpMJ7B/xNLcQ0Vj0N3/vm97RkD/8BJlYZoPAL
+ AAA=
+ http_version:
+ recorded_at: Thu, 09 Jun 2016 14:42:08 GMT
+recorded_with: VCR 3.0.1
diff --git a/spec/lib/carto/bano/address_retriever_spec.rb b/spec/lib/carto/bano/address_retriever_spec.rb
new file mode 100644
index 000000000..0f7797a0e
--- /dev/null
+++ b/spec/lib/carto/bano/address_retriever_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Carto::Bano::AddressRetriever do
+ describe '#list' do
+ let(:request) { 'Paris' }
+ let(:response) { File.open('spec/support/files/ban_address_search.json') }
+ let(:status) { 200 }
+
+ subject { described_class.new(request).list }
+
+ before do
+ stub_request(:get, "http://api-adresse.data.gouv.fr/search?&q=#{request}&limit=5")
+ .to_return(status: status, body: response, headers: {})
+ end
+
+ context 'when address return a list of address' do
+ it { expect(subject.size).to eq 5 }
+ it { is_expected.to be_an_instance_of Array }
+ end
+
+ context 'when address return an empty list' do
+ let(:response) { File.open('spec/support/files/ban_address_search_no_result.json') }
+
+ it { expect(subject.size).to eq 0 }
+ it { is_expected.to be_an_instance_of Array }
+ end
+
+ context 'when BAN is unavailable' do
+ let(:status) { 503 }
+ let(:response) { '' }
+
+ it { expect(subject.size).to eq 0 }
+ it { is_expected.to be_an_instance_of Array }
+ end
+
+ context 'when request is empty' do
+ let(:response) { 'Missing query' }
+ let(:request) { '' }
+
+ it { expect(subject.size).to eq 0 }
+ it { is_expected.to be_an_instance_of Array }
+ end
+ end
+end
diff --git a/spec/models/champ_spec.rb b/spec/models/champ_spec.rb
index 4216eb359..0e2cd61a8 100644
--- a/spec/models/champ_spec.rb
+++ b/spec/models/champ_spec.rb
@@ -15,4 +15,26 @@ describe Champ do
it { is_expected.to delegate_method(:type_champ).to(:type_de_champ) }
it { is_expected.to delegate_method(:order_place).to(:type_de_champ) }
end
+
+ describe 'data_provide' do
+ let(:champ) { create :champ }
+
+ subject { champ.data_provide }
+
+ context 'when type_champ is datetime' do
+ before do
+ champ.type_de_champ = create :type_de_champ, type_champ: 'datetime'
+ end
+
+ it { is_expected.to eq 'datepicker' }
+ end
+
+ context 'when type_champ is address' do
+ before do
+ champ.type_de_champ = create :type_de_champ, type_champ: 'address'
+ end
+
+ it { is_expected.to eq 'typeahead' }
+ end
+ end
end
\ No newline at end of file
diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb
index 6b6118104..b36e64837 100644
--- a/spec/models/procedure_spec.rb
+++ b/spec/models/procedure_spec.rb
@@ -79,28 +79,47 @@ describe Procedure do
end
describe 'locked?' do
- let(:procedure) { create(:procedure) }
+ let(:procedure) { create(:procedure, published: published) }
subject { procedure.locked? }
- context 'when procedure does not have dossier' do
+ context 'when procedure is in draft status' do
+ let(:published) { false }
it { is_expected.to be_falsey }
end
- context 'when procedure have dossier with state draft' do
- before do
- create(:dossier, procedure: procedure, state: :draft)
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'when procedure have dossier with state initiated' do
- before do
- create(:dossier, procedure: procedure, state: :initiated)
- end
-
+ context 'when procedure is in draft status' do
+ let(:published) { true }
it { is_expected.to be_truthy }
end
end
+
+ describe 'active' do
+ let(:procedure) { create(:procedure, published: published, archived: archived) }
+ subject { Procedure.active(procedure.id) }
+
+ context 'when procedure is in draft status and not archived' do
+ let(:published) { false }
+ let(:archived) { false }
+ it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
+ end
+
+ context 'when procedure is published and not archived' do
+ let(:published) { true }
+ let(:archived) { false }
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when procedure is published and archived' do
+ let(:published) { true }
+ let(:archived) { true }
+ it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
+ end
+
+ context 'when procedure is in draft status and archived' do
+ let(:published) { false }
+ let(:archived) { true }
+ it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
+ end
+ end
end
diff --git a/spec/models/type_de_piece_justificative_spec.rb b/spec/models/type_de_piece_justificative_spec.rb
index 0fe11970e..a2b47c14e 100644
--- a/spec/models/type_de_piece_justificative_spec.rb
+++ b/spec/models/type_de_piece_justificative_spec.rb
@@ -9,6 +9,7 @@ describe TypeDePieceJustificative do
it { is_expected.to have_db_column(:api_entreprise) }
it { is_expected.to have_db_column(:created_at) }
it { is_expected.to have_db_column(:updated_at) }
+ it { is_expected.to have_db_column(:order_place) }
end
describe 'associations' do
@@ -22,5 +23,11 @@ describe TypeDePieceJustificative do
it { is_expected.not_to allow_value('').for(:libelle) }
it { is_expected.to allow_value('RIB').for(:libelle) }
end
+
+ context 'order_place' do
+ # it { is_expected.not_to allow_value(nil).for(:order_place) }
+ # it { is_expected.not_to allow_value('').for(:order_place) }
+ it { is_expected.to allow_value(1).for(:order_place) }
+ end
end
end
diff --git a/spec/support/files/ban_address_search.json b/spec/support/files/ban_address_search.json
new file mode 100644
index 000000000..ac00ec114
--- /dev/null
+++ b/spec/support/files/ban_address_search.json
@@ -0,0 +1,117 @@
+{
+ "limit": 5,
+ "attribution": "BAN",
+ "version": "draft",
+ "licence": "ODbL 1.0",
+ "query": "Paris",
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 2.3469,
+ 48.8589
+ ]
+ },
+ "properties": {
+ "adm_weight": "6",
+ "citycode": "75056",
+ "name": "Paris",
+ "city": "Paris",
+ "postcode": "75000",
+ "context": "75, \u00cele-de-France",
+ "score": 1.0,
+ "label": "Paris",
+ "id": "75056",
+ "type": "city",
+ "population": "2244"
+ },
+ "type": "Feature"
+ },
+ {
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 4.366801,
+ 44.425528
+ ]
+ },
+ "properties": {
+ "citycode": "07330",
+ "postcode": "07150",
+ "name": "Paris",
+ "id": "07330_B095_bd3524",
+ "context": "07, Ard\u00e8che, Rh\u00f4ne-Alpes",
+ "score": 0.8291454545454544,
+ "label": "Paris 07150 Vallon-Pont-d'Arc",
+ "city": "Vallon-Pont-d'Arc",
+ "type": "locality"
+ },
+ "type": "Feature"
+ },
+ {
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 3.564293,
+ 45.766413
+ ]
+ },
+ "properties": {
+ "citycode": "63125",
+ "postcode": "63120",
+ "name": "Paris",
+ "city": "Courpi\u00e8re",
+ "context": "63, Puy-de-D\u00f4me, Auvergne",
+ "score": 0.8255363636363636,
+ "label": "Paris 63120 Courpi\u00e8re",
+ "id": "63125_B221_03549b",
+ "type": "locality"
+ },
+ "type": "Feature"
+ },
+ {
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.550208,
+ 44.673592
+ ]
+ },
+ "properties": {
+ "citycode": "46138",
+ "postcode": "46240",
+ "name": "PARIS (Vaillac)",
+ "city": "C\u0153ur de Causse",
+ "context": "46, Lot, Midi-Pyr\u00e9n\u00e9es",
+ "score": 0.824090909090909,
+ "label": "PARIS (Vaillac) 46240 C\u0153ur de Causse",
+ "id": "46138_XXXX_6ee4ec",
+ "type": "street"
+ },
+ "type": "Feature"
+ },
+ {
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ -0.526884,
+ 43.762253
+ ]
+ },
+ "properties": {
+ "citycode": "40282",
+ "postcode": "40500",
+ "name": "Paris",
+ "city": "Saint-Sever",
+ "context": "40, Landes, Aquitaine",
+ "score": 0.8236181818181818,
+ "label": "Paris 40500 Saint-Sever",
+ "id": "40282_B237_2364e3",
+ "type": "locality"
+ },
+ "type": "Feature"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/spec/support/files/ban_address_search_no_result.json b/spec/support/files/ban_address_search_no_result.json
new file mode 100644
index 000000000..1c2078f75
--- /dev/null
+++ b/spec/support/files/ban_address_search_no_result.json
@@ -0,0 +1,9 @@
+{
+ "limit": 5,
+ "attribution": "BAN",
+ "version": "draft",
+ "licence": "ODbL 1.0",
+ "query": "Paris",
+ "type": "FeatureCollection",
+ "features": []
+}
\ No newline at end of file
diff --git a/spec/views/admin/procedures/show.html.haml_spec.rb b/spec/views/admin/procedures/show.html.haml_spec.rb
index 3f3474aa8..7e5c272a1 100644
--- a/spec/views/admin/procedures/show.html.haml_spec.rb
+++ b/spec/views/admin/procedures/show.html.haml_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe 'admin/procedures/show.html.haml', type: :view do
- let(:procedure) { create(:procedure) }
+ let(:archived) { false }
+ let(:published) { false }
+ let(:procedure) { create(:procedure, published: published, archived: archived) }
before do
assign(:facade, AdminProceduresShowFacades.new(procedure.decorate))
@@ -9,19 +11,32 @@ describe 'admin/procedures/show.html.haml', type: :view do
render
end
+ describe 'publish button' do
+ it { expect(rendered).to have_content('Publier') }
+ end
+
describe 'archive and unarchive button' do
- context 'when procedure is active' do
+ let(:published) { true }
+
+ context 'when procedure is published' do
it { expect(rendered).to have_content('Archiver') }
end
context 'when procedure is archived' do
- let(:procedure) { create(:procedure, archived: true) }
-
+ let(:archived) { true }
it { expect(rendered).to have_content('Réactiver') }
end
end
- describe 'procedure link is present' do
- it { expect(rendered).to have_content(new_users_dossiers_url(procedure_id: procedure.id)) }
+ describe 'procedure link' do
+
+ context 'is not present when not published' do
+ it { expect(rendered).to have_content('Cette procédure n\'a pas encore été publiée et n\'est donc pas accessible par le public.') }
+ end
+
+ context 'is present when already published' do
+ let(:published) { true }
+ it { expect(rendered).to have_content(new_users_dossiers_url(procedure_id: procedure.id)) }
+ end
end
end
\ No newline at end of file
diff --git a/spec/views/admin/types_de_piece_justificative/show.html.haml_spec.rb b/spec/views/admin/types_de_piece_justificative/show.html.haml_spec.rb
new file mode 100644
index 000000000..6434a7ddd
--- /dev/null
+++ b/spec/views/admin/types_de_piece_justificative/show.html.haml_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe 'admin/pieces_justificatives/show.html.haml', type: :view do
+ let(:procedure) { create(:procedure) }
+
+ describe 'fields sorted' do
+ let(:first_libelle) { 'salut la compagnie' }
+ let(:last_libelle) { 'je suis bien sur la page' }
+ let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1, libelle: last_libelle) }
+ let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0, libelle: first_libelle) }
+ before do
+ procedure.reload
+ assign(:procedure, procedure)
+ render
+ end
+ it 'sorts by order place' do
+ expect(rendered).to match(/#{first_libelle}.*#{last_libelle}/m)
+ end
+ end
+
+ describe 'arrow button' do
+ subject do
+ procedure.reload
+ assign(:procedure, procedure)
+ render
+ rendered
+ end
+ context 'when there is no field in database' do
+ it { expect(subject).not_to have_css('.fa-chevron-down') }
+ it { expect(subject).not_to have_css('.fa-chevron-up') }
+ end
+ context 'when there is only one field in database' do
+ let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
+ it { expect(subject).not_to have_css('#btn_down_0') }
+ it { expect(subject).not_to have_css('#btn_up_0') }
+ it { expect(subject).not_to have_css('#btn_up_1') }
+ it { expect(subject).not_to have_css('#btn_down_1') }
+ end
+ context 'when there are 2 fields in database' do
+ let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
+ let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
+ it { expect(subject).to have_css('#btn_down_0') }
+ it { expect(subject).not_to have_css('#btn_up_0') }
+ it { expect(subject).to have_css('#btn_up_1') }
+ it { expect(subject).not_to have_css('#btn_down_1') }
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/views/layouts/_navbar_spec.rb b/spec/views/layouts/_navbar_spec.rb
new file mode 100644
index 000000000..3c3156d6d
--- /dev/null
+++ b/spec/views/layouts/_navbar_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe 'layouts/_navbar.html.haml', type: :view do
+ let(:administrateur) { create(:administrateur) }
+ let(:gestionnaire) { create(:gestionnaire, administrateurs: [administrateur]) }
+
+ let!(:procedure) { create(:procedure, administrateur: administrateur) }
+
+ describe 'navbar entries' do
+
+ context 'when disconnected' do
+ before do
+ render
+ end
+ subject { rendered }
+ it { is_expected.to match(/href="\/users\/sign_in">Utilisateur/) }
+ it { is_expected.to match(/href="\/gestionnaires\/sign_in">Accompagnateur/) }
+ it { is_expected.to match(/href="\/administrateurs\/sign_in">Administrateur/) }
+ it { is_expected.not_to match(/Mes Dossiers/) }
+ it { is_expected.not_to match(/Mes Procédures/) }
+ it { is_expected.not_to match(/Se déconnecter/) }
+ end
+
+ context 'when administrateur is connected' do
+ before do
+ @request.env["devise.mapping"] = Devise.mappings[:administrateur]
+ @current_user = administrateur
+ sign_in @current_user
+ render
+ end
+
+ subject { rendered }
+ it { is_expected.not_to match(/href="\/users\/sign_in">Utilisateur/) }
+ it { is_expected.not_to match(/href="\/gestionnaires\/sign_in">Accompagnateur/) }
+ it { is_expected.not_to match(/href="\/administrateurs\/sign_in">Administrateur/) }
+ it { is_expected.not_to match(/Mes Dossiers/) }
+ it { is_expected.to match(/Mes Procédures/) }
+ it { is_expected.to match(/Se déconnecter/) }
+ end
+
+ context 'when gestionnaire is connected' do
+ before do
+ @request.env["devise.mapping"] = Devise.mappings[:gestionnaire]
+ @current_user = gestionnaire
+ sign_in @current_user
+ render
+ end
+
+ subject { rendered }
+ it { is_expected.not_to match(/href="\/users\/sign_in">Utilisateur/) }
+ it { is_expected.not_to match(/href="\/gestionnaires\/sign_in">Accompagnateur/) }
+ it { is_expected.not_to match(/href="\/administrateurs\/sign_in">Administrateur/) }
+ it { is_expected.not_to match(/Mes Procédures/) }
+ it { is_expected.to match(/Mes Dossiers/) }
+ it { is_expected.to match(/Se déconnecter/) }
+ end
+
+ end
+end
diff --git a/vendor/assets/javascripts/typeahead.bundle.js b/vendor/assets/javascripts/typeahead.bundle.js
new file mode 100644
index 000000000..64b508ac5
--- /dev/null
+++ b/vendor/assets/javascripts/typeahead.bundle.js
@@ -0,0 +1,2451 @@
+/*!
+ * typeahead.js 0.11.1
+ * https://github.com/twitter/typeahead.js
+ * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT
+ */
+
+(function(root, factory) {
+ if (typeof define === "function" && define.amd) {
+ define("bloodhound", [ "jquery" ], function(a0) {
+ return root["Bloodhound"] = factory(a0);
+ });
+ } else if (typeof exports === "object") {
+ module.exports = factory(require("jquery"));
+ } else {
+ root["Bloodhound"] = factory(jQuery);
+ }
+})(this, function($) {
+ var _ = function() {
+ "use strict";
+ return {
+ isMsie: function() {
+ return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
+ },
+ isBlankString: function(str) {
+ return !str || /^\s*$/.test(str);
+ },
+ escapeRegExChars: function(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+ isString: function(obj) {
+ return typeof obj === "string";
+ },
+ isNumber: function(obj) {
+ return typeof obj === "number";
+ },
+ isArray: $.isArray,
+ isFunction: $.isFunction,
+ isObject: $.isPlainObject,
+ isUndefined: function(obj) {
+ return typeof obj === "undefined";
+ },
+ isElement: function(obj) {
+ return !!(obj && obj.nodeType === 1);
+ },
+ isJQuery: function(obj) {
+ return obj instanceof $;
+ },
+ toStr: function toStr(s) {
+ return _.isUndefined(s) || s === null ? "" : s + "";
+ },
+ bind: $.proxy,
+ each: function(collection, cb) {
+ $.each(collection, reverseArgs);
+ function reverseArgs(index, value) {
+ return cb(value, index);
+ }
+ },
+ map: $.map,
+ filter: $.grep,
+ every: function(obj, test) {
+ var result = true;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (!(result = test.call(null, val, key, obj))) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ some: function(obj, test) {
+ var result = false;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (result = test.call(null, val, key, obj)) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ mixin: $.extend,
+ identity: function(x) {
+ return x;
+ },
+ clone: function(obj) {
+ return $.extend(true, {}, obj);
+ },
+ getIdGenerator: function() {
+ var counter = 0;
+ return function() {
+ return counter++;
+ };
+ },
+ templatify: function templatify(obj) {
+ return $.isFunction(obj) ? obj : template;
+ function template() {
+ return String(obj);
+ }
+ },
+ defer: function(fn) {
+ setTimeout(fn, 0);
+ },
+ debounce: function(func, wait, immediate) {
+ var timeout, result;
+ return function() {
+ var context = this, args = arguments, later, callNow;
+ later = function() {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ }
+ };
+ callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ }
+ return result;
+ };
+ },
+ throttle: function(func, wait) {
+ var context, args, timeout, result, previous, later;
+ previous = 0;
+ later = function() {
+ previous = new Date();
+ timeout = null;
+ result = func.apply(context, args);
+ };
+ return function() {
+ var now = new Date(), remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ },
+ stringify: function(val) {
+ return _.isString(val) ? val : JSON.stringify(val);
+ },
+ noop: function() {}
+ };
+ }();
+ var VERSION = "0.11.1";
+ var tokenizers = function() {
+ "use strict";
+ return {
+ nonword: nonword,
+ whitespace: whitespace,
+ obj: {
+ nonword: getObjTokenizer(nonword),
+ whitespace: getObjTokenizer(whitespace)
+ }
+ };
+ function whitespace(str) {
+ str = _.toStr(str);
+ return str ? str.split(/\s+/) : [];
+ }
+ function nonword(str) {
+ str = _.toStr(str);
+ return str ? str.split(/\W+/) : [];
+ }
+ function getObjTokenizer(tokenizer) {
+ return function setKey(keys) {
+ keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
+ return function tokenize(o) {
+ var tokens = [];
+ _.each(keys, function(k) {
+ tokens = tokens.concat(tokenizer(_.toStr(o[k])));
+ });
+ return tokens;
+ };
+ };
+ }
+ }();
+ var LruCache = function() {
+ "use strict";
+ function LruCache(maxSize) {
+ this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
+ this.reset();
+ if (this.maxSize <= 0) {
+ this.set = this.get = $.noop;
+ }
+ }
+ _.mixin(LruCache.prototype, {
+ set: function set(key, val) {
+ var tailItem = this.list.tail, node;
+ if (this.size >= this.maxSize) {
+ this.list.remove(tailItem);
+ delete this.hash[tailItem.key];
+ this.size--;
+ }
+ if (node = this.hash[key]) {
+ node.val = val;
+ this.list.moveToFront(node);
+ } else {
+ node = new Node(key, val);
+ this.list.add(node);
+ this.hash[key] = node;
+ this.size++;
+ }
+ },
+ get: function get(key) {
+ var node = this.hash[key];
+ if (node) {
+ this.list.moveToFront(node);
+ return node.val;
+ }
+ },
+ reset: function reset() {
+ this.size = 0;
+ this.hash = {};
+ this.list = new List();
+ }
+ });
+ function List() {
+ this.head = this.tail = null;
+ }
+ _.mixin(List.prototype, {
+ add: function add(node) {
+ if (this.head) {
+ node.next = this.head;
+ this.head.prev = node;
+ }
+ this.head = node;
+ this.tail = this.tail || node;
+ },
+ remove: function remove(node) {
+ node.prev ? node.prev.next = node.next : this.head = node.next;
+ node.next ? node.next.prev = node.prev : this.tail = node.prev;
+ },
+ moveToFront: function(node) {
+ this.remove(node);
+ this.add(node);
+ }
+ });
+ function Node(key, val) {
+ this.key = key;
+ this.val = val;
+ this.prev = this.next = null;
+ }
+ return LruCache;
+ }();
+ var PersistentStorage = function() {
+ "use strict";
+ var LOCAL_STORAGE;
+ try {
+ LOCAL_STORAGE = window.localStorage;
+ LOCAL_STORAGE.setItem("~~~", "!");
+ LOCAL_STORAGE.removeItem("~~~");
+ } catch (err) {
+ LOCAL_STORAGE = null;
+ }
+ function PersistentStorage(namespace, override) {
+ this.prefix = [ "__", namespace, "__" ].join("");
+ this.ttlKey = "__ttl__";
+ this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix));
+ this.ls = override || LOCAL_STORAGE;
+ !this.ls && this._noop();
+ }
+ _.mixin(PersistentStorage.prototype, {
+ _prefix: function(key) {
+ return this.prefix + key;
+ },
+ _ttlKey: function(key) {
+ return this._prefix(key) + this.ttlKey;
+ },
+ _noop: function() {
+ this.get = this.set = this.remove = this.clear = this.isExpired = _.noop;
+ },
+ _safeSet: function(key, val) {
+ try {
+ this.ls.setItem(key, val);
+ } catch (err) {
+ if (err.name === "QuotaExceededError") {
+ this.clear();
+ this._noop();
+ }
+ }
+ },
+ get: function(key) {
+ if (this.isExpired(key)) {
+ this.remove(key);
+ }
+ return decode(this.ls.getItem(this._prefix(key)));
+ },
+ set: function(key, val, ttl) {
+ if (_.isNumber(ttl)) {
+ this._safeSet(this._ttlKey(key), encode(now() + ttl));
+ } else {
+ this.ls.removeItem(this._ttlKey(key));
+ }
+ return this._safeSet(this._prefix(key), encode(val));
+ },
+ remove: function(key) {
+ this.ls.removeItem(this._ttlKey(key));
+ this.ls.removeItem(this._prefix(key));
+ return this;
+ },
+ clear: function() {
+ var i, keys = gatherMatchingKeys(this.keyMatcher);
+ for (i = keys.length; i--; ) {
+ this.remove(keys[i]);
+ }
+ return this;
+ },
+ isExpired: function(key) {
+ var ttl = decode(this.ls.getItem(this._ttlKey(key)));
+ return _.isNumber(ttl) && now() > ttl ? true : false;
+ }
+ });
+ return PersistentStorage;
+ function now() {
+ return new Date().getTime();
+ }
+ function encode(val) {
+ return JSON.stringify(_.isUndefined(val) ? null : val);
+ }
+ function decode(val) {
+ return $.parseJSON(val);
+ }
+ function gatherMatchingKeys(keyMatcher) {
+ var i, key, keys = [], len = LOCAL_STORAGE.length;
+ for (i = 0; i < len; i++) {
+ if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
+ keys.push(key.replace(keyMatcher, ""));
+ }
+ }
+ return keys;
+ }
+ }();
+ var Transport = function() {
+ "use strict";
+ var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10);
+ function Transport(o) {
+ o = o || {};
+ this.cancelled = false;
+ this.lastReq = null;
+ this._send = o.transport;
+ this._get = o.limiter ? o.limiter(this._get) : this._get;
+ this._cache = o.cache === false ? new LruCache(0) : sharedCache;
+ }
+ Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
+ maxPendingRequests = num;
+ };
+ Transport.resetCache = function resetCache() {
+ sharedCache.reset();
+ };
+ _.mixin(Transport.prototype, {
+ _fingerprint: function fingerprint(o) {
+ o = o || {};
+ return o.url + o.type + $.param(o.data || {});
+ },
+ _get: function(o, cb) {
+ var that = this, fingerprint, jqXhr;
+ fingerprint = this._fingerprint(o);
+ if (this.cancelled || fingerprint !== this.lastReq) {
+ return;
+ }
+ if (jqXhr = pendingRequests[fingerprint]) {
+ jqXhr.done(done).fail(fail);
+ } else if (pendingRequestsCount < maxPendingRequests) {
+ pendingRequestsCount++;
+ pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
+ } else {
+ this.onDeckRequestArgs = [].slice.call(arguments, 0);
+ }
+ function done(resp) {
+ cb(null, resp);
+ that._cache.set(fingerprint, resp);
+ }
+ function fail() {
+ cb(true);
+ }
+ function always() {
+ pendingRequestsCount--;
+ delete pendingRequests[fingerprint];
+ if (that.onDeckRequestArgs) {
+ that._get.apply(that, that.onDeckRequestArgs);
+ that.onDeckRequestArgs = null;
+ }
+ }
+ },
+ get: function(o, cb) {
+ var resp, fingerprint;
+ cb = cb || $.noop;
+ o = _.isString(o) ? {
+ url: o
+ } : o || {};
+ fingerprint = this._fingerprint(o);
+ this.cancelled = false;
+ this.lastReq = fingerprint;
+ if (resp = this._cache.get(fingerprint)) {
+ cb(null, resp);
+ } else {
+ this._get(o, cb);
+ }
+ },
+ cancel: function() {
+ this.cancelled = true;
+ }
+ });
+ return Transport;
+ }();
+ var SearchIndex = window.SearchIndex = function() {
+ "use strict";
+ var CHILDREN = "c", IDS = "i";
+ function SearchIndex(o) {
+ o = o || {};
+ if (!o.datumTokenizer || !o.queryTokenizer) {
+ $.error("datumTokenizer and queryTokenizer are both required");
+ }
+ this.identify = o.identify || _.stringify;
+ this.datumTokenizer = o.datumTokenizer;
+ this.queryTokenizer = o.queryTokenizer;
+ this.reset();
+ }
+ _.mixin(SearchIndex.prototype, {
+ bootstrap: function bootstrap(o) {
+ this.datums = o.datums;
+ this.trie = o.trie;
+ },
+ add: function(data) {
+ var that = this;
+ data = _.isArray(data) ? data : [ data ];
+ _.each(data, function(datum) {
+ var id, tokens;
+ that.datums[id = that.identify(datum)] = datum;
+ tokens = normalizeTokens(that.datumTokenizer(datum));
+ _.each(tokens, function(token) {
+ var node, chars, ch;
+ node = that.trie;
+ chars = token.split("");
+ while (ch = chars.shift()) {
+ node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode());
+ node[IDS].push(id);
+ }
+ });
+ });
+ },
+ get: function get(ids) {
+ var that = this;
+ return _.map(ids, function(id) {
+ return that.datums[id];
+ });
+ },
+ search: function search(query) {
+ var that = this, tokens, matches;
+ tokens = normalizeTokens(this.queryTokenizer(query));
+ _.each(tokens, function(token) {
+ var node, chars, ch, ids;
+ if (matches && matches.length === 0) {
+ return false;
+ }
+ node = that.trie;
+ chars = token.split("");
+ while (node && (ch = chars.shift())) {
+ node = node[CHILDREN][ch];
+ }
+ if (node && chars.length === 0) {
+ ids = node[IDS].slice(0);
+ matches = matches ? getIntersection(matches, ids) : ids;
+ } else {
+ matches = [];
+ return false;
+ }
+ });
+ return matches ? _.map(unique(matches), function(id) {
+ return that.datums[id];
+ }) : [];
+ },
+ all: function all() {
+ var values = [];
+ for (var key in this.datums) {
+ values.push(this.datums[key]);
+ }
+ return values;
+ },
+ reset: function reset() {
+ this.datums = {};
+ this.trie = newNode();
+ },
+ serialize: function serialize() {
+ return {
+ datums: this.datums,
+ trie: this.trie
+ };
+ }
+ });
+ return SearchIndex;
+ function normalizeTokens(tokens) {
+ tokens = _.filter(tokens, function(token) {
+ return !!token;
+ });
+ tokens = _.map(tokens, function(token) {
+ return token.toLowerCase();
+ });
+ return tokens;
+ }
+ function newNode() {
+ var node = {};
+ node[IDS] = [];
+ node[CHILDREN] = {};
+ return node;
+ }
+ function unique(array) {
+ var seen = {}, uniques = [];
+ for (var i = 0, len = array.length; i < len; i++) {
+ if (!seen[array[i]]) {
+ seen[array[i]] = true;
+ uniques.push(array[i]);
+ }
+ }
+ return uniques;
+ }
+ function getIntersection(arrayA, arrayB) {
+ var ai = 0, bi = 0, intersection = [];
+ arrayA = arrayA.sort();
+ arrayB = arrayB.sort();
+ var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
+ while (ai < lenArrayA && bi < lenArrayB) {
+ if (arrayA[ai] < arrayB[bi]) {
+ ai++;
+ } else if (arrayA[ai] > arrayB[bi]) {
+ bi++;
+ } else {
+ intersection.push(arrayA[ai]);
+ ai++;
+ bi++;
+ }
+ }
+ return intersection;
+ }
+ }();
+ var Prefetch = function() {
+ "use strict";
+ var keys;
+ keys = {
+ data: "data",
+ protocol: "protocol",
+ thumbprint: "thumbprint"
+ };
+ function Prefetch(o) {
+ this.url = o.url;
+ this.ttl = o.ttl;
+ this.cache = o.cache;
+ this.prepare = o.prepare;
+ this.transform = o.transform;
+ this.transport = o.transport;
+ this.thumbprint = o.thumbprint;
+ this.storage = new PersistentStorage(o.cacheKey);
+ }
+ _.mixin(Prefetch.prototype, {
+ _settings: function settings() {
+ return {
+ url: this.url,
+ type: "GET",
+ dataType: "json"
+ };
+ },
+ store: function store(data) {
+ if (!this.cache) {
+ return;
+ }
+ this.storage.set(keys.data, data, this.ttl);
+ this.storage.set(keys.protocol, location.protocol, this.ttl);
+ this.storage.set(keys.thumbprint, this.thumbprint, this.ttl);
+ },
+ fromCache: function fromCache() {
+ var stored = {}, isExpired;
+ if (!this.cache) {
+ return null;
+ }
+ stored.data = this.storage.get(keys.data);
+ stored.protocol = this.storage.get(keys.protocol);
+ stored.thumbprint = this.storage.get(keys.thumbprint);
+ isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol;
+ return stored.data && !isExpired ? stored.data : null;
+ },
+ fromNetwork: function(cb) {
+ var that = this, settings;
+ if (!cb) {
+ return;
+ }
+ settings = this.prepare(this._settings());
+ this.transport(settings).fail(onError).done(onResponse);
+ function onError() {
+ cb(true);
+ }
+ function onResponse(resp) {
+ cb(null, that.transform(resp));
+ }
+ },
+ clear: function clear() {
+ this.storage.clear();
+ return this;
+ }
+ });
+ return Prefetch;
+ }();
+ var Remote = function() {
+ "use strict";
+ function Remote(o) {
+ this.url = o.url;
+ this.prepare = o.prepare;
+ this.transform = o.transform;
+ this.transport = new Transport({
+ cache: o.cache,
+ limiter: o.limiter,
+ transport: o.transport
+ });
+ }
+ _.mixin(Remote.prototype, {
+ _settings: function settings() {
+ return {
+ url: this.url,
+ type: "GET",
+ dataType: "json"
+ };
+ },
+ get: function get(query, cb) {
+ var that = this, settings;
+ if (!cb) {
+ return;
+ }
+ query = query || "";
+ settings = this.prepare(query, this._settings());
+ return this.transport.get(settings, onResponse);
+ function onResponse(err, resp) {
+ err ? cb([]) : cb(that.transform(resp));
+ }
+ },
+ cancelLastRequest: function cancelLastRequest() {
+ this.transport.cancel();
+ }
+ });
+ return Remote;
+ }();
+ var oParser = function() {
+ "use strict";
+ return function parse(o) {
+ var defaults, sorter;
+ defaults = {
+ initialize: true,
+ identify: _.stringify,
+ datumTokenizer: null,
+ queryTokenizer: null,
+ sufficient: 5,
+ sorter: null,
+ local: [],
+ prefetch: null,
+ remote: null
+ };
+ o = _.mixin(defaults, o || {});
+ !o.datumTokenizer && $.error("datumTokenizer is required");
+ !o.queryTokenizer && $.error("queryTokenizer is required");
+ sorter = o.sorter;
+ o.sorter = sorter ? function(x) {
+ return x.sort(sorter);
+ } : _.identity;
+ o.local = _.isFunction(o.local) ? o.local() : o.local;
+ o.prefetch = parsePrefetch(o.prefetch);
+ o.remote = parseRemote(o.remote);
+ return o;
+ };
+ function parsePrefetch(o) {
+ var defaults;
+ if (!o) {
+ return null;
+ }
+ defaults = {
+ url: null,
+ ttl: 24 * 60 * 60 * 1e3,
+ cache: true,
+ cacheKey: null,
+ thumbprint: "",
+ prepare: _.identity,
+ transform: _.identity,
+ transport: null
+ };
+ o = _.isString(o) ? {
+ url: o
+ } : o;
+ o = _.mixin(defaults, o);
+ !o.url && $.error("prefetch requires url to be set");
+ o.transform = o.filter || o.transform;
+ o.cacheKey = o.cacheKey || o.url;
+ o.thumbprint = VERSION + o.thumbprint;
+ o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
+ return o;
+ }
+ function parseRemote(o) {
+ var defaults;
+ if (!o) {
+ return;
+ }
+ defaults = {
+ url: null,
+ cache: true,
+ prepare: null,
+ replace: null,
+ wildcard: null,
+ limiter: null,
+ rateLimitBy: "debounce",
+ rateLimitWait: 300,
+ transform: _.identity,
+ transport: null
+ };
+ o = _.isString(o) ? {
+ url: o
+ } : o;
+ o = _.mixin(defaults, o);
+ !o.url && $.error("remote requires url to be set");
+ o.transform = o.filter || o.transform;
+ o.prepare = toRemotePrepare(o);
+ o.limiter = toLimiter(o);
+ o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
+ delete o.replace;
+ delete o.wildcard;
+ delete o.rateLimitBy;
+ delete o.rateLimitWait;
+ return o;
+ }
+ function toRemotePrepare(o) {
+ var prepare, replace, wildcard;
+ prepare = o.prepare;
+ replace = o.replace;
+ wildcard = o.wildcard;
+ if (prepare) {
+ return prepare;
+ }
+ if (replace) {
+ prepare = prepareByReplace;
+ } else if (o.wildcard) {
+ prepare = prepareByWildcard;
+ } else {
+ prepare = idenityPrepare;
+ }
+ return prepare;
+ function prepareByReplace(query, settings) {
+ settings.url = replace(settings.url, query);
+ return settings;
+ }
+ function prepareByWildcard(query, settings) {
+ settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
+ return settings;
+ }
+ function idenityPrepare(query, settings) {
+ return settings;
+ }
+ }
+ function toLimiter(o) {
+ var limiter, method, wait;
+ limiter = o.limiter;
+ method = o.rateLimitBy;
+ wait = o.rateLimitWait;
+ if (!limiter) {
+ limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait);
+ }
+ return limiter;
+ function debounce(wait) {
+ return function debounce(fn) {
+ return _.debounce(fn, wait);
+ };
+ }
+ function throttle(wait) {
+ return function throttle(fn) {
+ return _.throttle(fn, wait);
+ };
+ }
+ }
+ function callbackToDeferred(fn) {
+ return function wrapper(o) {
+ var deferred = $.Deferred();
+ fn(o, onSuccess, onError);
+ return deferred;
+ function onSuccess(resp) {
+ _.defer(function() {
+ deferred.resolve(resp);
+ });
+ }
+ function onError(err) {
+ _.defer(function() {
+ deferred.reject(err);
+ });
+ }
+ };
+ }
+ }();
+ var Bloodhound = function() {
+ "use strict";
+ var old;
+ old = window && window.Bloodhound;
+ function Bloodhound(o) {
+ o = oParser(o);
+ this.sorter = o.sorter;
+ this.identify = o.identify;
+ this.sufficient = o.sufficient;
+ this.local = o.local;
+ this.remote = o.remote ? new Remote(o.remote) : null;
+ this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
+ this.index = new SearchIndex({
+ identify: this.identify,
+ datumTokenizer: o.datumTokenizer,
+ queryTokenizer: o.queryTokenizer
+ });
+ o.initialize !== false && this.initialize();
+ }
+ Bloodhound.noConflict = function noConflict() {
+ window && (window.Bloodhound = old);
+ return Bloodhound;
+ };
+ Bloodhound.tokenizers = tokenizers;
+ _.mixin(Bloodhound.prototype, {
+ __ttAdapter: function ttAdapter() {
+ var that = this;
+ return this.remote ? withAsync : withoutAsync;
+ function withAsync(query, sync, async) {
+ return that.search(query, sync, async);
+ }
+ function withoutAsync(query, sync) {
+ return that.search(query, sync);
+ }
+ },
+ _loadPrefetch: function loadPrefetch() {
+ var that = this, deferred, serialized;
+ deferred = $.Deferred();
+ if (!this.prefetch) {
+ deferred.resolve();
+ } else if (serialized = this.prefetch.fromCache()) {
+ this.index.bootstrap(serialized);
+ deferred.resolve();
+ } else {
+ this.prefetch.fromNetwork(done);
+ }
+ return deferred.promise();
+ function done(err, data) {
+ if (err) {
+ return deferred.reject();
+ }
+ that.add(data);
+ that.prefetch.store(that.index.serialize());
+ deferred.resolve();
+ }
+ },
+ _initialize: function initialize() {
+ var that = this, deferred;
+ this.clear();
+ (this.initPromise = this._loadPrefetch()).done(addLocalToIndex);
+ return this.initPromise;
+ function addLocalToIndex() {
+ that.add(that.local);
+ }
+ },
+ initialize: function initialize(force) {
+ return !this.initPromise || force ? this._initialize() : this.initPromise;
+ },
+ add: function add(data) {
+ this.index.add(data);
+ return this;
+ },
+ get: function get(ids) {
+ ids = _.isArray(ids) ? ids : [].slice.call(arguments);
+ return this.index.get(ids);
+ },
+ search: function search(query, sync, async) {
+ var that = this, local;
+ local = this.sorter(this.index.search(query));
+ sync(this.remote ? local.slice() : local);
+ if (this.remote && local.length < this.sufficient) {
+ this.remote.get(query, processRemote);
+ } else if (this.remote) {
+ this.remote.cancelLastRequest();
+ }
+ return this;
+ function processRemote(remote) {
+ var nonDuplicates = [];
+ _.each(remote, function(r) {
+ !_.some(local, function(l) {
+ return that.identify(r) === that.identify(l);
+ }) && nonDuplicates.push(r);
+ });
+ async && async(nonDuplicates);
+ }
+ },
+ all: function all() {
+ return this.index.all();
+ },
+ clear: function clear() {
+ this.index.reset();
+ return this;
+ },
+ clearPrefetchCache: function clearPrefetchCache() {
+ this.prefetch && this.prefetch.clear();
+ return this;
+ },
+ clearRemoteCache: function clearRemoteCache() {
+ Transport.resetCache();
+ return this;
+ },
+ ttAdapter: function ttAdapter() {
+ return this.__ttAdapter();
+ }
+ });
+ return Bloodhound;
+ }();
+ return Bloodhound;
+});
+
+(function(root, factory) {
+ if (typeof define === "function" && define.amd) {
+ define("typeahead.js", [ "jquery" ], function(a0) {
+ return factory(a0);
+ });
+ } else if (typeof exports === "object") {
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+})(this, function($) {
+ var _ = function() {
+ "use strict";
+ return {
+ isMsie: function() {
+ return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
+ },
+ isBlankString: function(str) {
+ return !str || /^\s*$/.test(str);
+ },
+ escapeRegExChars: function(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+ isString: function(obj) {
+ return typeof obj === "string";
+ },
+ isNumber: function(obj) {
+ return typeof obj === "number";
+ },
+ isArray: $.isArray,
+ isFunction: $.isFunction,
+ isObject: $.isPlainObject,
+ isUndefined: function(obj) {
+ return typeof obj === "undefined";
+ },
+ isElement: function(obj) {
+ return !!(obj && obj.nodeType === 1);
+ },
+ isJQuery: function(obj) {
+ return obj instanceof $;
+ },
+ toStr: function toStr(s) {
+ return _.isUndefined(s) || s === null ? "" : s + "";
+ },
+ bind: $.proxy,
+ each: function(collection, cb) {
+ $.each(collection, reverseArgs);
+ function reverseArgs(index, value) {
+ return cb(value, index);
+ }
+ },
+ map: $.map,
+ filter: $.grep,
+ every: function(obj, test) {
+ var result = true;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (!(result = test.call(null, val, key, obj))) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ some: function(obj, test) {
+ var result = false;
+ if (!obj) {
+ return result;
+ }
+ $.each(obj, function(key, val) {
+ if (result = test.call(null, val, key, obj)) {
+ return false;
+ }
+ });
+ return !!result;
+ },
+ mixin: $.extend,
+ identity: function(x) {
+ return x;
+ },
+ clone: function(obj) {
+ return $.extend(true, {}, obj);
+ },
+ getIdGenerator: function() {
+ var counter = 0;
+ return function() {
+ return counter++;
+ };
+ },
+ templatify: function templatify(obj) {
+ return $.isFunction(obj) ? obj : template;
+ function template() {
+ return String(obj);
+ }
+ },
+ defer: function(fn) {
+ setTimeout(fn, 0);
+ },
+ debounce: function(func, wait, immediate) {
+ var timeout, result;
+ return function() {
+ var context = this, args = arguments, later, callNow;
+ later = function() {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ }
+ };
+ callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ }
+ return result;
+ };
+ },
+ throttle: function(func, wait) {
+ var context, args, timeout, result, previous, later;
+ previous = 0;
+ later = function() {
+ previous = new Date();
+ timeout = null;
+ result = func.apply(context, args);
+ };
+ return function() {
+ var now = new Date(), remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ },
+ stringify: function(val) {
+ return _.isString(val) ? val : JSON.stringify(val);
+ },
+ noop: function() {}
+ };
+ }();
+ var WWW = function() {
+ "use strict";
+ var defaultClassNames = {
+ wrapper: "twitter-typeahead",
+ input: "tt-input",
+ hint: "tt-hint",
+ menu: "tt-menu",
+ dataset: "tt-dataset",
+ suggestion: "tt-suggestion",
+ selectable: "tt-selectable",
+ empty: "tt-empty",
+ open: "tt-open",
+ cursor: "tt-cursor",
+ highlight: "tt-highlight"
+ };
+ return build;
+ function build(o) {
+ var www, classes;
+ classes = _.mixin({}, defaultClassNames, o);
+ www = {
+ css: buildCss(),
+ classes: classes,
+ html: buildHtml(classes),
+ selectors: buildSelectors(classes)
+ };
+ return {
+ css: www.css,
+ html: www.html,
+ classes: www.classes,
+ selectors: www.selectors,
+ mixin: function(o) {
+ _.mixin(o, www);
+ }
+ };
+ }
+ function buildHtml(c) {
+ return {
+ wrapper: '',
+ menu: ''
+ };
+ }
+ function buildSelectors(classes) {
+ var selectors = {};
+ _.each(classes, function(v, k) {
+ selectors[k] = "." + v;
+ });
+ return selectors;
+ }
+ function buildCss() {
+ var css = {
+ wrapper: {
+ position: "relative",
+ display: "inline-block"
+ },
+ hint: {
+ position: "absolute",
+ top: "0",
+ left: "0",
+ borderColor: "transparent",
+ boxShadow: "none",
+ opacity: "1"
+ },
+ input: {
+ position: "relative",
+ verticalAlign: "top",
+ backgroundColor: "transparent"
+ },
+ inputWithNoHint: {
+ position: "relative",
+ verticalAlign: "top"
+ },
+ menu: {
+ position: "absolute",
+ top: "100%",
+ left: "0",
+ zIndex: "100",
+ display: "none"
+ },
+ ltr: {
+ left: "0",
+ right: "auto"
+ },
+ rtl: {
+ left: "auto",
+ right: " 0"
+ }
+ };
+ if (_.isMsie()) {
+ _.mixin(css.input, {
+ backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
+ });
+ }
+ return css;
+ }
+ }();
+ var EventBus = function() {
+ "use strict";
+ var namespace, deprecationMap;
+ namespace = "typeahead:";
+ deprecationMap = {
+ render: "rendered",
+ cursorchange: "cursorchanged",
+ select: "selected",
+ autocomplete: "autocompleted"
+ };
+ function EventBus(o) {
+ if (!o || !o.el) {
+ $.error("EventBus initialized without el");
+ }
+ this.$el = $(o.el);
+ }
+ _.mixin(EventBus.prototype, {
+ _trigger: function(type, args) {
+ var $e;
+ $e = $.Event(namespace + type);
+ (args = args || []).unshift($e);
+ this.$el.trigger.apply(this.$el, args);
+ return $e;
+ },
+ before: function(type) {
+ var args, $e;
+ args = [].slice.call(arguments, 1);
+ $e = this._trigger("before" + type, args);
+ return $e.isDefaultPrevented();
+ },
+ trigger: function(type) {
+ var deprecatedType;
+ this._trigger(type, [].slice.call(arguments, 1));
+ if (deprecatedType = deprecationMap[type]) {
+ this._trigger(deprecatedType, [].slice.call(arguments, 1));
+ }
+ }
+ });
+ return EventBus;
+ }();
+ var EventEmitter = function() {
+ "use strict";
+ var splitter = /\s+/, nextTick = getNextTick();
+ return {
+ onSync: onSync,
+ onAsync: onAsync,
+ off: off,
+ trigger: trigger
+ };
+ function on(method, types, cb, context) {
+ var type;
+ if (!cb) {
+ return this;
+ }
+ types = types.split(splitter);
+ cb = context ? bindContext(cb, context) : cb;
+ this._callbacks = this._callbacks || {};
+ while (type = types.shift()) {
+ this._callbacks[type] = this._callbacks[type] || {
+ sync: [],
+ async: []
+ };
+ this._callbacks[type][method].push(cb);
+ }
+ return this;
+ }
+ function onAsync(types, cb, context) {
+ return on.call(this, "async", types, cb, context);
+ }
+ function onSync(types, cb, context) {
+ return on.call(this, "sync", types, cb, context);
+ }
+ function off(types) {
+ var type;
+ if (!this._callbacks) {
+ return this;
+ }
+ types = types.split(splitter);
+ while (type = types.shift()) {
+ delete this._callbacks[type];
+ }
+ return this;
+ }
+ function trigger(types) {
+ var type, callbacks, args, syncFlush, asyncFlush;
+ if (!this._callbacks) {
+ return this;
+ }
+ types = types.split(splitter);
+ args = [].slice.call(arguments, 1);
+ while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
+ syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
+ asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
+ syncFlush() && nextTick(asyncFlush);
+ }
+ return this;
+ }
+ function getFlush(callbacks, context, args) {
+ return flush;
+ function flush() {
+ var cancelled;
+ for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) {
+ cancelled = callbacks[i].apply(context, args) === false;
+ }
+ return !cancelled;
+ }
+ }
+ function getNextTick() {
+ var nextTickFn;
+ if (window.setImmediate) {
+ nextTickFn = function nextTickSetImmediate(fn) {
+ setImmediate(function() {
+ fn();
+ });
+ };
+ } else {
+ nextTickFn = function nextTickSetTimeout(fn) {
+ setTimeout(function() {
+ fn();
+ }, 0);
+ };
+ }
+ return nextTickFn;
+ }
+ function bindContext(fn, context) {
+ return fn.bind ? fn.bind(context) : function() {
+ fn.apply(context, [].slice.call(arguments, 0));
+ };
+ }
+ }();
+ var highlight = function(doc) {
+ "use strict";
+ var defaults = {
+ node: null,
+ pattern: null,
+ tagName: "strong",
+ className: null,
+ wordsOnly: false,
+ caseSensitive: false
+ };
+ return function hightlight(o) {
+ var regex;
+ o = _.mixin({}, defaults, o);
+ if (!o.node || !o.pattern) {
+ return;
+ }
+ o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
+ regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
+ traverse(o.node, hightlightTextNode);
+ function hightlightTextNode(textNode) {
+ var match, patternNode, wrapperNode;
+ if (match = regex.exec(textNode.data)) {
+ wrapperNode = doc.createElement(o.tagName);
+ o.className && (wrapperNode.className = o.className);
+ patternNode = textNode.splitText(match.index);
+ patternNode.splitText(match[0].length);
+ wrapperNode.appendChild(patternNode.cloneNode(true));
+ textNode.parentNode.replaceChild(wrapperNode, patternNode);
+ }
+ return !!match;
+ }
+ function traverse(el, hightlightTextNode) {
+ var childNode, TEXT_NODE_TYPE = 3;
+ for (var i = 0; i < el.childNodes.length; i++) {
+ childNode = el.childNodes[i];
+ if (childNode.nodeType === TEXT_NODE_TYPE) {
+ i += hightlightTextNode(childNode) ? 1 : 0;
+ } else {
+ traverse(childNode, hightlightTextNode);
+ }
+ }
+ }
+ };
+ function getRegex(patterns, caseSensitive, wordsOnly) {
+ var escapedPatterns = [], regexStr;
+ for (var i = 0, len = patterns.length; i < len; i++) {
+ escapedPatterns.push(_.escapeRegExChars(patterns[i]));
+ }
+ regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
+ return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
+ }
+ }(window.document);
+ var Input = function() {
+ "use strict";
+ var specialKeyCodeMap;
+ specialKeyCodeMap = {
+ 9: "tab",
+ 27: "esc",
+ 37: "left",
+ 39: "right",
+ 13: "enter",
+ 38: "up",
+ 40: "down"
+ };
+ function Input(o, www) {
+ o = o || {};
+ if (!o.input) {
+ $.error("input is missing");
+ }
+ www.mixin(this);
+ this.$hint = $(o.hint);
+ this.$input = $(o.input);
+ this.query = this.$input.val();
+ this.queryWhenFocused = this.hasFocus() ? this.query : null;
+ this.$overflowHelper = buildOverflowHelper(this.$input);
+ this._checkLanguageDirection();
+ if (this.$hint.length === 0) {
+ this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
+ }
+ }
+ Input.normalizeQuery = function(str) {
+ return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
+ };
+ _.mixin(Input.prototype, EventEmitter, {
+ _onBlur: function onBlur() {
+ this.resetInputValue();
+ this.trigger("blurred");
+ },
+ _onFocus: function onFocus() {
+ this.queryWhenFocused = this.query;
+ this.trigger("focused");
+ },
+ _onKeydown: function onKeydown($e) {
+ var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
+ this._managePreventDefault(keyName, $e);
+ if (keyName && this._shouldTrigger(keyName, $e)) {
+ this.trigger(keyName + "Keyed", $e);
+ }
+ },
+ _onInput: function onInput() {
+ this._setQuery(this.getInputValue());
+ this.clearHintIfInvalid();
+ this._checkLanguageDirection();
+ },
+ _managePreventDefault: function managePreventDefault(keyName, $e) {
+ var preventDefault;
+ switch (keyName) {
+ case "up":
+ case "down":
+ preventDefault = !withModifier($e);
+ break;
+
+ default:
+ preventDefault = false;
+ }
+ preventDefault && $e.preventDefault();
+ },
+ _shouldTrigger: function shouldTrigger(keyName, $e) {
+ var trigger;
+ switch (keyName) {
+ case "tab":
+ trigger = !withModifier($e);
+ break;
+
+ default:
+ trigger = true;
+ }
+ return trigger;
+ },
+ _checkLanguageDirection: function checkLanguageDirection() {
+ var dir = (this.$input.css("direction") || "ltr").toLowerCase();
+ if (this.dir !== dir) {
+ this.dir = dir;
+ this.$hint.attr("dir", dir);
+ this.trigger("langDirChanged", dir);
+ }
+ },
+ _setQuery: function setQuery(val, silent) {
+ var areEquivalent, hasDifferentWhitespace;
+ areEquivalent = areQueriesEquivalent(val, this.query);
+ hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false;
+ this.query = val;
+ if (!silent && !areEquivalent) {
+ this.trigger("queryChanged", this.query);
+ } else if (!silent && hasDifferentWhitespace) {
+ this.trigger("whitespaceChanged", this.query);
+ }
+ },
+ bind: function() {
+ var that = this, onBlur, onFocus, onKeydown, onInput;
+ onBlur = _.bind(this._onBlur, this);
+ onFocus = _.bind(this._onFocus, this);
+ onKeydown = _.bind(this._onKeydown, this);
+ onInput = _.bind(this._onInput, this);
+ this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
+ if (!_.isMsie() || _.isMsie() > 9) {
+ this.$input.on("input.tt", onInput);
+ } else {
+ this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
+ if (specialKeyCodeMap[$e.which || $e.keyCode]) {
+ return;
+ }
+ _.defer(_.bind(that._onInput, that, $e));
+ });
+ }
+ return this;
+ },
+ focus: function focus() {
+ this.$input.focus();
+ },
+ blur: function blur() {
+ this.$input.blur();
+ },
+ getLangDir: function getLangDir() {
+ return this.dir;
+ },
+ getQuery: function getQuery() {
+ return this.query || "";
+ },
+ setQuery: function setQuery(val, silent) {
+ this.setInputValue(val);
+ this._setQuery(val, silent);
+ },
+ hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() {
+ return this.query !== this.queryWhenFocused;
+ },
+ getInputValue: function getInputValue() {
+ return this.$input.val();
+ },
+ setInputValue: function setInputValue(value) {
+ this.$input.val(value);
+ this.clearHintIfInvalid();
+ this._checkLanguageDirection();
+ },
+ resetInputValue: function resetInputValue() {
+ this.setInputValue(this.query);
+ },
+ getHint: function getHint() {
+ return this.$hint.val();
+ },
+ setHint: function setHint(value) {
+ this.$hint.val(value);
+ },
+ clearHint: function clearHint() {
+ this.setHint("");
+ },
+ clearHintIfInvalid: function clearHintIfInvalid() {
+ var val, hint, valIsPrefixOfHint, isValid;
+ val = this.getInputValue();
+ hint = this.getHint();
+ valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
+ isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
+ !isValid && this.clearHint();
+ },
+ hasFocus: function hasFocus() {
+ return this.$input.is(":focus");
+ },
+ hasOverflow: function hasOverflow() {
+ var constraint = this.$input.width() - 2;
+ this.$overflowHelper.text(this.getInputValue());
+ return this.$overflowHelper.width() >= constraint;
+ },
+ isCursorAtEnd: function() {
+ var valueLength, selectionStart, range;
+ valueLength = this.$input.val().length;
+ selectionStart = this.$input[0].selectionStart;
+ if (_.isNumber(selectionStart)) {
+ return selectionStart === valueLength;
+ } else if (document.selection) {
+ range = document.selection.createRange();
+ range.moveStart("character", -valueLength);
+ return valueLength === range.text.length;
+ }
+ return true;
+ },
+ destroy: function destroy() {
+ this.$hint.off(".tt");
+ this.$input.off(".tt");
+ this.$overflowHelper.remove();
+ this.$hint = this.$input = this.$overflowHelper = $("");
+ }
+ });
+ return Input;
+ function buildOverflowHelper($input) {
+ return $('
').css({
+ position: "absolute",
+ visibility: "hidden",
+ whiteSpace: "pre",
+ fontFamily: $input.css("font-family"),
+ fontSize: $input.css("font-size"),
+ fontStyle: $input.css("font-style"),
+ fontVariant: $input.css("font-variant"),
+ fontWeight: $input.css("font-weight"),
+ wordSpacing: $input.css("word-spacing"),
+ letterSpacing: $input.css("letter-spacing"),
+ textIndent: $input.css("text-indent"),
+ textRendering: $input.css("text-rendering"),
+ textTransform: $input.css("text-transform")
+ }).insertAfter($input);
+ }
+ function areQueriesEquivalent(a, b) {
+ return Input.normalizeQuery(a) === Input.normalizeQuery(b);
+ }
+ function withModifier($e) {
+ return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
+ }
+ }();
+ var Dataset = function() {
+ "use strict";
+ var keys, nameGenerator;
+ keys = {
+ val: "tt-selectable-display",
+ obj: "tt-selectable-object"
+ };
+ nameGenerator = _.getIdGenerator();
+ function Dataset(o, www) {
+ o = o || {};
+ o.templates = o.templates || {};
+ o.templates.notFound = o.templates.notFound || o.templates.empty;
+ if (!o.source) {
+ $.error("missing source");
+ }
+ if (!o.node) {
+ $.error("missing node");
+ }
+ if (o.name && !isValidName(o.name)) {
+ $.error("invalid dataset name: " + o.name);
+ }
+ www.mixin(this);
+ this.highlight = !!o.highlight;
+ this.name = o.name || nameGenerator();
+ this.limit = o.limit || 5;
+ this.displayFn = getDisplayFn(o.display || o.displayKey);
+ this.templates = getTemplates(o.templates, this.displayFn);
+ this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
+ this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
+ this._resetLastSuggestion();
+ this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
+ }
+ Dataset.extractData = function extractData(el) {
+ var $el = $(el);
+ if ($el.data(keys.obj)) {
+ return {
+ val: $el.data(keys.val) || "",
+ obj: $el.data(keys.obj) || null
+ };
+ }
+ return null;
+ };
+ _.mixin(Dataset.prototype, EventEmitter, {
+ _overwrite: function overwrite(query, suggestions) {
+ suggestions = suggestions || [];
+ if (suggestions.length) {
+ this._renderSuggestions(query, suggestions);
+ } else if (this.async && this.templates.pending) {
+ this._renderPending(query);
+ } else if (!this.async && this.templates.notFound) {
+ this._renderNotFound(query);
+ } else {
+ this._empty();
+ }
+ this.trigger("rendered", this.name, suggestions, false);
+ },
+ _append: function append(query, suggestions) {
+ suggestions = suggestions || [];
+ if (suggestions.length && this.$lastSuggestion.length) {
+ this._appendSuggestions(query, suggestions);
+ } else if (suggestions.length) {
+ this._renderSuggestions(query, suggestions);
+ } else if (!this.$lastSuggestion.length && this.templates.notFound) {
+ this._renderNotFound(query);
+ }
+ this.trigger("rendered", this.name, suggestions, true);
+ },
+ _renderSuggestions: function renderSuggestions(query, suggestions) {
+ var $fragment;
+ $fragment = this._getSuggestionsFragment(query, suggestions);
+ this.$lastSuggestion = $fragment.children().last();
+ this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions));
+ },
+ _appendSuggestions: function appendSuggestions(query, suggestions) {
+ var $fragment, $lastSuggestion;
+ $fragment = this._getSuggestionsFragment(query, suggestions);
+ $lastSuggestion = $fragment.children().last();
+ this.$lastSuggestion.after($fragment);
+ this.$lastSuggestion = $lastSuggestion;
+ },
+ _renderPending: function renderPending(query) {
+ var template = this.templates.pending;
+ this._resetLastSuggestion();
+ template && this.$el.html(template({
+ query: query,
+ dataset: this.name
+ }));
+ },
+ _renderNotFound: function renderNotFound(query) {
+ var template = this.templates.notFound;
+ this._resetLastSuggestion();
+ template && this.$el.html(template({
+ query: query,
+ dataset: this.name
+ }));
+ },
+ _empty: function empty() {
+ this.$el.empty();
+ this._resetLastSuggestion();
+ },
+ _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) {
+ var that = this, fragment;
+ fragment = document.createDocumentFragment();
+ _.each(suggestions, function getSuggestionNode(suggestion) {
+ var $el, context;
+ context = that._injectQuery(query, suggestion);
+ $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
+ fragment.appendChild($el[0]);
+ });
+ this.highlight && highlight({
+ className: this.classes.highlight,
+ node: fragment,
+ pattern: query
+ });
+ return $(fragment);
+ },
+ _getFooter: function getFooter(query, suggestions) {
+ return this.templates.footer ? this.templates.footer({
+ query: query,
+ suggestions: suggestions,
+ dataset: this.name
+ }) : null;
+ },
+ _getHeader: function getHeader(query, suggestions) {
+ return this.templates.header ? this.templates.header({
+ query: query,
+ suggestions: suggestions,
+ dataset: this.name
+ }) : null;
+ },
+ _resetLastSuggestion: function resetLastSuggestion() {
+ this.$lastSuggestion = $();
+ },
+ _injectQuery: function injectQuery(query, obj) {
+ return _.isObject(obj) ? _.mixin({
+ _query: query
+ }, obj) : obj;
+ },
+ update: function update(query) {
+ var that = this, canceled = false, syncCalled = false, rendered = 0;
+ this.cancel();
+ this.cancel = function cancel() {
+ canceled = true;
+ that.cancel = $.noop;
+ that.async && that.trigger("asyncCanceled", query);
+ };
+ this.source(query, sync, async);
+ !syncCalled && sync([]);
+ function sync(suggestions) {
+ if (syncCalled) {
+ return;
+ }
+ syncCalled = true;
+ suggestions = (suggestions || []).slice(0, that.limit);
+ rendered = suggestions.length;
+ that._overwrite(query, suggestions);
+ if (rendered < that.limit && that.async) {
+ that.trigger("asyncRequested", query);
+ }
+ }
+ function async(suggestions) {
+ suggestions = suggestions || [];
+ if (!canceled && rendered < that.limit) {
+ that.cancel = $.noop;
+ rendered += suggestions.length;
+ that._append(query, suggestions.slice(0, that.limit));
+ that.async && that.trigger("asyncReceived", query);
+ }
+ }
+ },
+ cancel: $.noop,
+ clear: function clear() {
+ this._empty();
+ this.cancel();
+ this.trigger("cleared");
+ },
+ isEmpty: function isEmpty() {
+ return this.$el.is(":empty");
+ },
+ destroy: function destroy() {
+ this.$el = $("
");
+ }
+ });
+ return Dataset;
+ function getDisplayFn(display) {
+ display = display || _.stringify;
+ return _.isFunction(display) ? display : displayFn;
+ function displayFn(obj) {
+ return obj[display];
+ }
+ }
+ function getTemplates(templates, displayFn) {
+ return {
+ notFound: templates.notFound && _.templatify(templates.notFound),
+ pending: templates.pending && _.templatify(templates.pending),
+ header: templates.header && _.templatify(templates.header),
+ footer: templates.footer && _.templatify(templates.footer),
+ suggestion: templates.suggestion || suggestionTemplate
+ };
+ function suggestionTemplate(context) {
+ return $("
").text(displayFn(context));
+ }
+ }
+ function isValidName(str) {
+ return /^[_a-zA-Z0-9-]+$/.test(str);
+ }
+ }();
+ var Menu = function() {
+ "use strict";
+ function Menu(o, www) {
+ var that = this;
+ o = o || {};
+ if (!o.node) {
+ $.error("node is required");
+ }
+ www.mixin(this);
+ this.$node = $(o.node);
+ this.query = null;
+ this.datasets = _.map(o.datasets, initializeDataset);
+ function initializeDataset(oDataset) {
+ var node = that.$node.find(oDataset.node).first();
+ oDataset.node = node.length ? node : $("
").appendTo(that.$node);
+ return new Dataset(oDataset, www);
+ }
+ }
+ _.mixin(Menu.prototype, EventEmitter, {
+ _onSelectableClick: function onSelectableClick($e) {
+ this.trigger("selectableClicked", $($e.currentTarget));
+ },
+ _onRendered: function onRendered(type, dataset, suggestions, async) {
+ this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
+ this.trigger("datasetRendered", dataset, suggestions, async);
+ },
+ _onCleared: function onCleared() {
+ this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
+ this.trigger("datasetCleared");
+ },
+ _propagate: function propagate() {
+ this.trigger.apply(this, arguments);
+ },
+ _allDatasetsEmpty: function allDatasetsEmpty() {
+ return _.every(this.datasets, isDatasetEmpty);
+ function isDatasetEmpty(dataset) {
+ return dataset.isEmpty();
+ }
+ },
+ _getSelectables: function getSelectables() {
+ return this.$node.find(this.selectors.selectable);
+ },
+ _removeCursor: function _removeCursor() {
+ var $selectable = this.getActiveSelectable();
+ $selectable && $selectable.removeClass(this.classes.cursor);
+ },
+ _ensureVisible: function ensureVisible($el) {
+ var elTop, elBottom, nodeScrollTop, nodeHeight;
+ elTop = $el.position().top;
+ elBottom = elTop + $el.outerHeight(true);
+ nodeScrollTop = this.$node.scrollTop();
+ nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10);
+ if (elTop < 0) {
+ this.$node.scrollTop(nodeScrollTop + elTop);
+ } else if (nodeHeight < elBottom) {
+ this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight));
+ }
+ },
+ bind: function() {
+ var that = this, onSelectableClick;
+ onSelectableClick = _.bind(this._onSelectableClick, this);
+ this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
+ _.each(this.datasets, function(dataset) {
+ dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
+ });
+ return this;
+ },
+ isOpen: function isOpen() {
+ return this.$node.hasClass(this.classes.open);
+ },
+ open: function open() {
+ this.$node.addClass(this.classes.open);
+ },
+ close: function close() {
+ this.$node.removeClass(this.classes.open);
+ this._removeCursor();
+ },
+ setLanguageDirection: function setLanguageDirection(dir) {
+ this.$node.attr("dir", dir);
+ },
+ selectableRelativeToCursor: function selectableRelativeToCursor(delta) {
+ var $selectables, $oldCursor, oldIndex, newIndex;
+ $oldCursor = this.getActiveSelectable();
+ $selectables = this._getSelectables();
+ oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1;
+ newIndex = oldIndex + delta;
+ newIndex = (newIndex + 1) % ($selectables.length + 1) - 1;
+ newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex;
+ return newIndex === -1 ? null : $selectables.eq(newIndex);
+ },
+ setCursor: function setCursor($selectable) {
+ this._removeCursor();
+ if ($selectable = $selectable && $selectable.first()) {
+ $selectable.addClass(this.classes.cursor);
+ this._ensureVisible($selectable);
+ }
+ },
+ getSelectableData: function getSelectableData($el) {
+ return $el && $el.length ? Dataset.extractData($el) : null;
+ },
+ getActiveSelectable: function getActiveSelectable() {
+ var $selectable = this._getSelectables().filter(this.selectors.cursor).first();
+ return $selectable.length ? $selectable : null;
+ },
+ getTopSelectable: function getTopSelectable() {
+ var $selectable = this._getSelectables().first();
+ return $selectable.length ? $selectable : null;
+ },
+ update: function update(query) {
+ var isValidUpdate = query !== this.query;
+ if (isValidUpdate) {
+ this.query = query;
+ _.each(this.datasets, updateDataset);
+ }
+ return isValidUpdate;
+ function updateDataset(dataset) {
+ dataset.update(query);
+ }
+ },
+ empty: function empty() {
+ _.each(this.datasets, clearDataset);
+ this.query = null;
+ this.$node.addClass(this.classes.empty);
+ function clearDataset(dataset) {
+ dataset.clear();
+ }
+ },
+ destroy: function destroy() {
+ this.$node.off(".tt");
+ this.$node = $("
");
+ _.each(this.datasets, destroyDataset);
+ function destroyDataset(dataset) {
+ dataset.destroy();
+ }
+ }
+ });
+ return Menu;
+ }();
+ var DefaultMenu = function() {
+ "use strict";
+ var s = Menu.prototype;
+ function DefaultMenu() {
+ Menu.apply(this, [].slice.call(arguments, 0));
+ }
+ _.mixin(DefaultMenu.prototype, Menu.prototype, {
+ open: function open() {
+ !this._allDatasetsEmpty() && this._show();
+ return s.open.apply(this, [].slice.call(arguments, 0));
+ },
+ close: function close() {
+ this._hide();
+ return s.close.apply(this, [].slice.call(arguments, 0));
+ },
+ _onRendered: function onRendered() {
+ if (this._allDatasetsEmpty()) {
+ this._hide();
+ } else {
+ this.isOpen() && this._show();
+ }
+ return s._onRendered.apply(this, [].slice.call(arguments, 0));
+ },
+ _onCleared: function onCleared() {
+ if (this._allDatasetsEmpty()) {
+ this._hide();
+ } else {
+ this.isOpen() && this._show();
+ }
+ return s._onCleared.apply(this, [].slice.call(arguments, 0));
+ },
+ setLanguageDirection: function setLanguageDirection(dir) {
+ this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl);
+ return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0));
+ },
+ _hide: function hide() {
+ this.$node.hide();
+ },
+ _show: function show() {
+ this.$node.css("display", "block");
+ }
+ });
+ return DefaultMenu;
+ }();
+ var Typeahead = function() {
+ "use strict";
+ function Typeahead(o, www) {
+ var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged;
+ o = o || {};
+ if (!o.input) {
+ $.error("missing input");
+ }
+ if (!o.menu) {
+ $.error("missing menu");
+ }
+ if (!o.eventBus) {
+ $.error("missing event bus");
+ }
+ www.mixin(this);
+ this.eventBus = o.eventBus;
+ this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
+ this.input = o.input;
+ this.menu = o.menu;
+ this.enabled = true;
+ this.active = false;
+ this.input.hasFocus() && this.activate();
+ this.dir = this.input.getLangDir();
+ this._hacks();
+ this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this);
+ onFocused = c(this, "activate", "open", "_onFocused");
+ onBlurred = c(this, "deactivate", "_onBlurred");
+ onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed");
+ onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed");
+ onEscKeyed = c(this, "isActive", "_onEscKeyed");
+ onUpKeyed = c(this, "isActive", "open", "_onUpKeyed");
+ onDownKeyed = c(this, "isActive", "open", "_onDownKeyed");
+ onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed");
+ onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed");
+ onQueryChanged = c(this, "_openIfActive", "_onQueryChanged");
+ onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged");
+ this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this);
+ }
+ _.mixin(Typeahead.prototype, {
+ _hacks: function hacks() {
+ var $input, $menu;
+ $input = this.input.$input || $("
");
+ $menu = this.menu.$node || $("
");
+ $input.on("blur.tt", function($e) {
+ var active, isActive, hasActive;
+ active = document.activeElement;
+ isActive = $menu.is(active);
+ hasActive = $menu.has(active).length > 0;
+ if (_.isMsie() && (isActive || hasActive)) {
+ $e.preventDefault();
+ $e.stopImmediatePropagation();
+ _.defer(function() {
+ $input.focus();
+ });
+ }
+ });
+ $menu.on("mousedown.tt", function($e) {
+ $e.preventDefault();
+ });
+ },
+ _onSelectableClicked: function onSelectableClicked(type, $el) {
+ this.select($el);
+ },
+ _onDatasetCleared: function onDatasetCleared() {
+ this._updateHint();
+ },
+ _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) {
+ this._updateHint();
+ this.eventBus.trigger("render", suggestions, async, dataset);
+ },
+ _onAsyncRequested: function onAsyncRequested(type, dataset, query) {
+ this.eventBus.trigger("asyncrequest", query, dataset);
+ },
+ _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) {
+ this.eventBus.trigger("asynccancel", query, dataset);
+ },
+ _onAsyncReceived: function onAsyncReceived(type, dataset, query) {
+ this.eventBus.trigger("asyncreceive", query, dataset);
+ },
+ _onFocused: function onFocused() {
+ this._minLengthMet() && this.menu.update(this.input.getQuery());
+ },
+ _onBlurred: function onBlurred() {
+ if (this.input.hasQueryChangedSinceLastFocus()) {
+ this.eventBus.trigger("change", this.input.getQuery());
+ }
+ },
+ _onEnterKeyed: function onEnterKeyed(type, $e) {
+ var $selectable;
+ if ($selectable = this.menu.getActiveSelectable()) {
+ this.select($selectable) && $e.preventDefault();
+ }
+ },
+ _onTabKeyed: function onTabKeyed(type, $e) {
+ var $selectable;
+ if ($selectable = this.menu.getActiveSelectable()) {
+ this.select($selectable) && $e.preventDefault();
+ } else if ($selectable = this.menu.getTopSelectable()) {
+ this.autocomplete($selectable) && $e.preventDefault();
+ }
+ },
+ _onEscKeyed: function onEscKeyed() {
+ this.close();
+ },
+ _onUpKeyed: function onUpKeyed() {
+ this.moveCursor(-1);
+ },
+ _onDownKeyed: function onDownKeyed() {
+ this.moveCursor(+1);
+ },
+ _onLeftKeyed: function onLeftKeyed() {
+ if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
+ this.autocomplete(this.menu.getTopSelectable());
+ }
+ },
+ _onRightKeyed: function onRightKeyed() {
+ if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
+ this.autocomplete(this.menu.getTopSelectable());
+ }
+ },
+ _onQueryChanged: function onQueryChanged(e, query) {
+ this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty();
+ },
+ _onWhitespaceChanged: function onWhitespaceChanged() {
+ this._updateHint();
+ },
+ _onLangDirChanged: function onLangDirChanged(e, dir) {
+ if (this.dir !== dir) {
+ this.dir = dir;
+ this.menu.setLanguageDirection(dir);
+ }
+ },
+ _openIfActive: function openIfActive() {
+ this.isActive() && this.open();
+ },
+ _minLengthMet: function minLengthMet(query) {
+ query = _.isString(query) ? query : this.input.getQuery() || "";
+ return query.length >= this.minLength;
+ },
+ _updateHint: function updateHint() {
+ var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match;
+ $selectable = this.menu.getTopSelectable();
+ data = this.menu.getSelectableData($selectable);
+ val = this.input.getInputValue();
+ if (data && !_.isBlankString(val) && !this.input.hasOverflow()) {
+ query = Input.normalizeQuery(val);
+ escapedQuery = _.escapeRegExChars(query);
+ frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
+ match = frontMatchRegEx.exec(data.val);
+ match && this.input.setHint(val + match[1]);
+ } else {
+ this.input.clearHint();
+ }
+ },
+ isEnabled: function isEnabled() {
+ return this.enabled;
+ },
+ enable: function enable() {
+ this.enabled = true;
+ },
+ disable: function disable() {
+ this.enabled = false;
+ },
+ isActive: function isActive() {
+ return this.active;
+ },
+ activate: function activate() {
+ if (this.isActive()) {
+ return true;
+ } else if (!this.isEnabled() || this.eventBus.before("active")) {
+ return false;
+ } else {
+ this.active = true;
+ this.eventBus.trigger("active");
+ return true;
+ }
+ },
+ deactivate: function deactivate() {
+ if (!this.isActive()) {
+ return true;
+ } else if (this.eventBus.before("idle")) {
+ return false;
+ } else {
+ this.active = false;
+ this.close();
+ this.eventBus.trigger("idle");
+ return true;
+ }
+ },
+ isOpen: function isOpen() {
+ return this.menu.isOpen();
+ },
+ open: function open() {
+ if (!this.isOpen() && !this.eventBus.before("open")) {
+ this.menu.open();
+ this._updateHint();
+ this.eventBus.trigger("open");
+ }
+ return this.isOpen();
+ },
+ close: function close() {
+ if (this.isOpen() && !this.eventBus.before("close")) {
+ this.menu.close();
+ this.input.clearHint();
+ this.input.resetInputValue();
+ this.eventBus.trigger("close");
+ }
+ return !this.isOpen();
+ },
+ setVal: function setVal(val) {
+ this.input.setQuery(_.toStr(val));
+ },
+ getVal: function getVal() {
+ return this.input.getQuery();
+ },
+ select: function select($selectable) {
+ var data = this.menu.getSelectableData($selectable);
+ if (data && !this.eventBus.before("select", data.obj)) {
+ this.input.setQuery(data.val, true);
+ this.eventBus.trigger("select", data.obj);
+ this.close();
+ return true;
+ }
+ return false;
+ },
+ autocomplete: function autocomplete($selectable) {
+ var query, data, isValid;
+ query = this.input.getQuery();
+ data = this.menu.getSelectableData($selectable);
+ isValid = data && query !== data.val;
+ if (isValid && !this.eventBus.before("autocomplete", data.obj)) {
+ this.input.setQuery(data.val);
+ this.eventBus.trigger("autocomplete", data.obj);
+ return true;
+ }
+ return false;
+ },
+ moveCursor: function moveCursor(delta) {
+ var query, $candidate, data, payload, cancelMove;
+ query = this.input.getQuery();
+ $candidate = this.menu.selectableRelativeToCursor(delta);
+ data = this.menu.getSelectableData($candidate);
+ payload = data ? data.obj : null;
+ cancelMove = this._minLengthMet() && this.menu.update(query);
+ if (!cancelMove && !this.eventBus.before("cursorchange", payload)) {
+ this.menu.setCursor($candidate);
+ if (data) {
+ this.input.setInputValue(data.val);
+ } else {
+ this.input.resetInputValue();
+ this._updateHint();
+ }
+ this.eventBus.trigger("cursorchange", payload);
+ return true;
+ }
+ return false;
+ },
+ destroy: function destroy() {
+ this.input.destroy();
+ this.menu.destroy();
+ }
+ });
+ return Typeahead;
+ function c(ctx) {
+ var methods = [].slice.call(arguments, 1);
+ return function() {
+ var args = [].slice.call(arguments);
+ _.each(methods, function(method) {
+ return ctx[method].apply(ctx, args);
+ });
+ };
+ }
+ }();
+ (function() {
+ "use strict";
+ var old, keys, methods;
+ old = $.fn.typeahead;
+ keys = {
+ www: "tt-www",
+ attrs: "tt-attrs",
+ typeahead: "tt-typeahead"
+ };
+ methods = {
+ initialize: function initialize(o, datasets) {
+ var www;
+ datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
+ o = o || {};
+ www = WWW(o.classNames);
+ return this.each(attach);
+ function attach() {
+ var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor;
+ _.each(datasets, function(d) {
+ d.highlight = !!o.highlight;
+ });
+ $input = $(this);
+ $wrapper = $(www.html.wrapper);
+ $hint = $elOrNull(o.hint);
+ $menu = $elOrNull(o.menu);
+ defaultHint = o.hint !== false && !$hint;
+ defaultMenu = o.menu !== false && !$menu;
+ defaultHint && ($hint = buildHintFromInput($input, www));
+ defaultMenu && ($menu = $(www.html.menu).css(www.css.menu));
+ $hint && $hint.val("");
+ $input = prepInput($input, www);
+ if (defaultHint || defaultMenu) {
+ $wrapper.css(www.css.wrapper);
+ $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint);
+ $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null);
+ }
+ MenuConstructor = defaultMenu ? DefaultMenu : Menu;
+ eventBus = new EventBus({
+ el: $input
+ });
+ input = new Input({
+ hint: $hint,
+ input: $input
+ }, www);
+ menu = new MenuConstructor({
+ node: $menu,
+ datasets: datasets
+ }, www);
+ typeahead = new Typeahead({
+ input: input,
+ menu: menu,
+ eventBus: eventBus,
+ minLength: o.minLength
+ }, www);
+ $input.data(keys.www, www);
+ $input.data(keys.typeahead, typeahead);
+ }
+ },
+ isEnabled: function isEnabled() {
+ var enabled;
+ ttEach(this.first(), function(t) {
+ enabled = t.isEnabled();
+ });
+ return enabled;
+ },
+ enable: function enable() {
+ ttEach(this, function(t) {
+ t.enable();
+ });
+ return this;
+ },
+ disable: function disable() {
+ ttEach(this, function(t) {
+ t.disable();
+ });
+ return this;
+ },
+ isActive: function isActive() {
+ var active;
+ ttEach(this.first(), function(t) {
+ active = t.isActive();
+ });
+ return active;
+ },
+ activate: function activate() {
+ ttEach(this, function(t) {
+ t.activate();
+ });
+ return this;
+ },
+ deactivate: function deactivate() {
+ ttEach(this, function(t) {
+ t.deactivate();
+ });
+ return this;
+ },
+ isOpen: function isOpen() {
+ var open;
+ ttEach(this.first(), function(t) {
+ open = t.isOpen();
+ });
+ return open;
+ },
+ open: function open() {
+ ttEach(this, function(t) {
+ t.open();
+ });
+ return this;
+ },
+ close: function close() {
+ ttEach(this, function(t) {
+ t.close();
+ });
+ return this;
+ },
+ select: function select(el) {
+ var success = false, $el = $(el);
+ ttEach(this.first(), function(t) {
+ success = t.select($el);
+ });
+ return success;
+ },
+ autocomplete: function autocomplete(el) {
+ var success = false, $el = $(el);
+ ttEach(this.first(), function(t) {
+ success = t.autocomplete($el);
+ });
+ return success;
+ },
+ moveCursor: function moveCursoe(delta) {
+ var success = false;
+ ttEach(this.first(), function(t) {
+ success = t.moveCursor(delta);
+ });
+ return success;
+ },
+ val: function val(newVal) {
+ var query;
+ if (!arguments.length) {
+ ttEach(this.first(), function(t) {
+ query = t.getVal();
+ });
+ return query;
+ } else {
+ ttEach(this, function(t) {
+ t.setVal(newVal);
+ });
+ return this;
+ }
+ },
+ destroy: function destroy() {
+ ttEach(this, function(typeahead, $input) {
+ revert($input);
+ typeahead.destroy();
+ });
+ return this;
+ }
+ };
+ $.fn.typeahead = function(method) {
+ if (methods[method]) {
+ return methods[method].apply(this, [].slice.call(arguments, 1));
+ } else {
+ return methods.initialize.apply(this, arguments);
+ }
+ };
+ $.fn.typeahead.noConflict = function noConflict() {
+ $.fn.typeahead = old;
+ return this;
+ };
+ function ttEach($els, fn) {
+ $els.each(function() {
+ var $input = $(this), typeahead;
+ (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input);
+ });
+ }
+ function buildHintFromInput($input, www) {
+ return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({
+ autocomplete: "off",
+ spellcheck: "false",
+ tabindex: -1
+ });
+ }
+ function prepInput($input, www) {
+ $input.data(keys.attrs, {
+ dir: $input.attr("dir"),
+ autocomplete: $input.attr("autocomplete"),
+ spellcheck: $input.attr("spellcheck"),
+ style: $input.attr("style")
+ });
+ $input.addClass(www.classes.input).attr({
+ autocomplete: "off",
+ spellcheck: false
+ });
+ try {
+ !$input.attr("dir") && $input.attr("dir", "auto");
+ } catch (e) {}
+ return $input;
+ }
+ function getBackgroundStyles($el) {
+ return {
+ backgroundAttachment: $el.css("background-attachment"),
+ backgroundClip: $el.css("background-clip"),
+ backgroundColor: $el.css("background-color"),
+ backgroundImage: $el.css("background-image"),
+ backgroundOrigin: $el.css("background-origin"),
+ backgroundPosition: $el.css("background-position"),
+ backgroundRepeat: $el.css("background-repeat"),
+ backgroundSize: $el.css("background-size")
+ };
+ }
+ function revert($input) {
+ var www, $wrapper;
+ www = $input.data(keys.www);
+ $wrapper = $input.parent().filter(www.selectors.wrapper);
+ _.each($input.data(keys.attrs), function(val, key) {
+ _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
+ });
+ $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input);
+ if ($wrapper.length) {
+ $input.detach().insertAfter($wrapper);
+ $wrapper.remove();
+ }
+ }
+ function $elOrNull(obj) {
+ var isValid, $el;
+ isValid = _.isJQuery(obj) || _.isElement(obj);
+ $el = isValid ? $(obj).first() : [];
+ return $el.length ? $el : null;
+ }
+ })();
+});
\ No newline at end of file