biola_wcms_components 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/biola-wcms-components.js.coffee +2 -0
- data/app/assets/javascripts/components/forms/person_lookup.js.coffee +1 -1
- data/app/assets/javascripts/components/forms/tag_input.js.coffee +42 -0
- data/app/assets/stylesheets/{biola-wcms-components.scss → biola-wcms-components.css.scss} +2 -0
- data/app/assets/stylesheets/components/forms/_person_lookup.scss +0 -47
- data/app/assets/stylesheets/components/forms/_tag_input.scss +5 -0
- data/app/controllers/wcms_application_controller.rb +37 -0
- data/app/controllers/wcms_components/embedded_images_controller.rb +20 -0
- data/app/controllers/wcms_components/people_controller.rb +30 -0
- data/app/views/wcms_components/forms/_json_editor.html.slim +1 -1
- data/app/views/wcms_components/forms/_person_lookup.html.slim +12 -3
- data/app/views/wcms_components/forms/_presentation_data_editor.html.slim +3 -4
- data/app/views/wcms_components/forms/_related_object_tags.html.slim +12 -0
- data/app/views/wcms_components/forms/_tag_input.html.slim +26 -0
- data/app/views/wcms_components/forms/_yaml_editor.html.slim +1 -1
- data/app/views/wcms_components/shared/_embedded_image_uploader.html.slim +1 -1
- data/biola_wcms_components.gemspec +2 -0
- data/config/routes.rb +13 -0
- data/lib/biola_wcms_components.rb +3 -0
- data/lib/biola_wcms_components/version.rb +1 -1
- data/lib/components/cas_authentication.rb +87 -0
- data/vendor/assets/javascripts/bootstrap-tagsinput.js +580 -0
- data/vendor/assets/stylesheets/bootstrap-tagsinput.css +55 -0
- data/vendor/assets/stylesheets/typeahead.css +50 -0
- metadata +43 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4615d1bb2bee67a6f0a7457288fc2df6796045b
|
|
4
|
+
data.tar.gz: 4d1f46f67c613b239f3a940d062f494c954ec969
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abc4d90dfbd41be28d68801f5d7cbcd446ac7380e70960bbb3696b0c9a381a794d0c55ed23c1a7ffd1232bdda40f070315deb83fb1ed172ff73ce77c67bdd5d4
|
|
7
|
+
data.tar.gz: 259e675f5fdbddb58a1ba88d4c474c0d4b00e0a98ef7e2f53f466df82248843af985c0b643a882c334afe4613f64237c11ad620b024567c430ef6da5d2efad21
|
|
@@ -3,7 +3,7 @@ $(document).ready ->
|
|
|
3
3
|
people_search_url = $('.person-lookup').first().data('lookup-url')
|
|
4
4
|
|
|
5
5
|
peopleSearch = new Bloodhound(
|
|
6
|
-
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('
|
|
6
|
+
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('id')
|
|
7
7
|
queryTokenizer: Bloodhound.tokenizers.whitespace
|
|
8
8
|
# The prefetch url returns a list of all the faculty
|
|
9
9
|
prefetch: people_search_url
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
initializeTaginput = (selector) ->
|
|
2
|
+
$(selector).each ->
|
|
3
|
+
tagsinput = $(this)
|
|
4
|
+
tagsinputOptions = {}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# If typeahead key is set, initialize typeahead. Otherwise just initialize a normal tagsinput
|
|
8
|
+
# available parameters:
|
|
9
|
+
# :url => url to the remote json data
|
|
10
|
+
# :key => the key to use for each element in the json array
|
|
11
|
+
# :query_param => The parameter key we should use to append the search to the url
|
|
12
|
+
# Defaults to 'q'
|
|
13
|
+
#
|
|
14
|
+
if options = tagsinput.data('typeahead')
|
|
15
|
+
options.query_param ||= 'q'
|
|
16
|
+
|
|
17
|
+
typeaheadEngine = new Bloodhound
|
|
18
|
+
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(options.key)
|
|
19
|
+
queryTokenizer: Bloodhound.tokenizers.whitespace
|
|
20
|
+
remote: "#{options.url}?#{options.query_param}=%QUERY"
|
|
21
|
+
typeaheadEngine.initialize()
|
|
22
|
+
|
|
23
|
+
tagsinputOptions.typeaheadjs =
|
|
24
|
+
displayKey: options.key
|
|
25
|
+
valueKey: options.key
|
|
26
|
+
highlight: true
|
|
27
|
+
source: typeaheadEngine.ttAdapter()
|
|
28
|
+
|
|
29
|
+
# Initialize tagsinput with options
|
|
30
|
+
tagsinput.tagsinput tagsinputOptions
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
$(document).ready ->
|
|
35
|
+
|
|
36
|
+
# Initialize tagsinput on page load
|
|
37
|
+
initializeTaginput("input[data-role=tagsinput]")
|
|
38
|
+
|
|
39
|
+
$(".modal").on "shown.bs.modal", (e) ->
|
|
40
|
+
# Initialize tagsinputs underneath modal.
|
|
41
|
+
initializeTaginput("input[data-role=tagsinput]")
|
|
42
|
+
|
|
@@ -1,48 +1,5 @@
|
|
|
1
1
|
.person-lookup {
|
|
2
|
-
.typeahead, .tt-query, .tt-hint {
|
|
3
|
-
width: 396px;
|
|
4
|
-
height: 34px;
|
|
5
|
-
padding: 8px 12px;
|
|
6
|
-
font-size: 15px;
|
|
7
|
-
line-height: 15px;
|
|
8
|
-
border: 1px solid #ccc;
|
|
9
|
-
-webkit-border-radius: 4px;
|
|
10
|
-
-moz-border-radius: 4px;
|
|
11
|
-
border-radius: 4px;
|
|
12
|
-
outline: none;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.tt-query {
|
|
16
|
-
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
|
17
|
-
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
|
18
|
-
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.tt-hint {
|
|
22
|
-
color: #999
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.tt-dropdown-menu {
|
|
26
|
-
text-align: left;
|
|
27
|
-
width: 422px;
|
|
28
|
-
margin-top: 12px;
|
|
29
|
-
padding: 8px 0;
|
|
30
|
-
background-color: #fff;
|
|
31
|
-
border: 1px solid #ccc;
|
|
32
|
-
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
33
|
-
-webkit-border-radius: 4px;
|
|
34
|
-
-moz-border-radius: 4px;
|
|
35
|
-
border-radius: 4px;
|
|
36
|
-
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
|
37
|
-
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
|
38
|
-
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
2
|
.tt-suggestion, .tt-empty-message {
|
|
42
|
-
padding: 3px 20px;
|
|
43
|
-
font-size: 16px;
|
|
44
|
-
line-height: 24px;
|
|
45
|
-
|
|
46
3
|
.affiliations {
|
|
47
4
|
color: #999;
|
|
48
5
|
font-size: 12px;
|
|
@@ -61,10 +18,6 @@
|
|
|
61
18
|
}
|
|
62
19
|
|
|
63
20
|
.tt-suggestion.tt-cursor {
|
|
64
|
-
cursor: pointer;
|
|
65
|
-
color: #fff;
|
|
66
|
-
background-color: #0097cf;
|
|
67
|
-
|
|
68
21
|
.affiliations {
|
|
69
22
|
color: #fff;
|
|
70
23
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ApplicationController should inherit from this controller.
|
|
2
|
+
class WcmsApplicationController < ActionController::Base
|
|
3
|
+
include Pundit
|
|
4
|
+
|
|
5
|
+
protect_from_forgery with: :exception
|
|
6
|
+
|
|
7
|
+
before_action :authenticate!
|
|
8
|
+
after_action :verify_authorized
|
|
9
|
+
after_action :verify_policy_scoped, only: :index
|
|
10
|
+
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
|
11
|
+
|
|
12
|
+
layout -> { (@layout || :application).to_s }
|
|
13
|
+
|
|
14
|
+
helper_method :current_user
|
|
15
|
+
def current_user
|
|
16
|
+
authentication.user
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def authenticate!
|
|
22
|
+
authentication.perform or render_error_page(401)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def authentication
|
|
26
|
+
@authentication ||= CasAuthentication.new(session)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render_error_page(status)
|
|
30
|
+
render file: "#{Rails.root}/public/#{status}", formats: [:html], status: status, layout: false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def user_not_authorized
|
|
34
|
+
render_error_page(403)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class WcmsComponents::EmbeddedImagesController < ApplicationController
|
|
2
|
+
|
|
3
|
+
skip_after_action :verify_authorized
|
|
4
|
+
skip_after_action :verify_policy_scoped
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
# Anyone who is logged in should be able to access this.
|
|
8
|
+
@embedded_image = EmbeddedImage.new
|
|
9
|
+
|
|
10
|
+
file = params[:file]
|
|
11
|
+
@embedded_image.upload = file
|
|
12
|
+
|
|
13
|
+
if @embedded_image.save
|
|
14
|
+
render json: { filelink: @embedded_image.upload.url }
|
|
15
|
+
else
|
|
16
|
+
render json: { error: true, messages: @embedded_image.errors.full_messages}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class WcmsComponents::PeopleController < ApplicationController
|
|
2
|
+
|
|
3
|
+
skip_after_action :verify_authorized
|
|
4
|
+
skip_after_action :verify_policy_scoped
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
# For security reasons, this should only be available to employees.
|
|
8
|
+
if current_user.admin? || current_user.has_role?(:employee)
|
|
9
|
+
if params[:q].present?
|
|
10
|
+
@people = permitted_people.custom_search(params[:q]).asc(:first_name, :last_name).limit(20)
|
|
11
|
+
else
|
|
12
|
+
# If no query string is present, return all faculty for pre-cached data.
|
|
13
|
+
@people = permitted_people.where(affiliations: 'faculty').custom_search(params[:q]).asc(:first_name, :last_name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
render json: @people.map{|p| {id: p.id.to_s, name: p.name, email: p.biola_email, affiliations: p.affiliations.to_a.join(', '), image: p.profile_photo_url} }.to_json
|
|
17
|
+
else
|
|
18
|
+
user_not_authorized
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def permitted_people
|
|
26
|
+
# Return all people who are either employees or not private.
|
|
27
|
+
Person.where({'$or' => [{affiliations: ['employee'] }, {privacy: { '$ne' => true }}] })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -3,7 +3,7 @@ ruby:
|
|
|
3
3
|
attribute ||= nil
|
|
4
4
|
html_class ||= 'form-control json_editor'
|
|
5
5
|
value ||= nil
|
|
6
|
-
embedded_image_url ||=
|
|
6
|
+
embedded_image_url ||= wcms_components_embedded_images_url
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
= wcms_component("shared/embedded_image_uploader", embedded_image_url: embedded_image_url)
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
ruby:
|
|
2
|
-
|
|
2
|
+
form ||= nil
|
|
3
|
+
person_id_key ||= :person_id
|
|
4
|
+
lookup_url ||= wcms_components_people_url
|
|
5
|
+
placeholder ||= 'First or last name'
|
|
6
|
+
required ||= false
|
|
7
|
+
|
|
8
|
+
html_options = {placeholder: placeholder, required: required, class: 'form-control typeahead'}
|
|
3
9
|
|
|
4
10
|
- if lookup_url
|
|
5
11
|
.person-lookup data-lookup-url=lookup_url
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
- if form
|
|
13
|
+
= form.hidden_field person_id_key, class: 'hidden-person-id'
|
|
14
|
+
- else
|
|
15
|
+
= hidden_field_tag person_id_key, '', class: 'hidden-person-id'
|
|
16
|
+
= text_field_tag :person_name, '', html_options
|
|
8
17
|
|
|
9
18
|
- else
|
|
10
19
|
p
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/ Example:
|
|
2
2
|
/ = wcms_component "forms/presentation_data_editor",
|
|
3
|
-
/ schema: @generic_object.presentation_data_template.schema,
|
|
4
|
-
/ data: @generic_object.presentation_data,
|
|
5
3
|
/ form: f,
|
|
6
|
-
/
|
|
4
|
+
/ schema: @generic_object.presentation_data_template.schema,
|
|
5
|
+
/ presentation_data: @generic_object.presentation_data
|
|
7
6
|
/
|
|
8
7
|
|
|
9
8
|
ruby:
|
|
10
9
|
schema ||= []
|
|
11
10
|
presentation_data ||= {}
|
|
12
11
|
form ||= nil
|
|
13
|
-
embedded_image_url ||=
|
|
12
|
+
embedded_image_url ||= wcms_components_embedded_images_url
|
|
14
13
|
|
|
15
14
|
= wcms_component "shared/embedded_image_uploader",
|
|
16
15
|
embedded_image_url: embedded_image_url
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/ Example:
|
|
2
|
+
/ = wcms_component "forms/tag_input",
|
|
3
|
+
/ form: f,
|
|
4
|
+
/ attribute: :courses_string,
|
|
5
|
+
/ typeahead: { url: courses_path(format: :json), key: 'course_key'}
|
|
6
|
+
/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ruby:
|
|
10
|
+
form ||= nil
|
|
11
|
+
attribute ||= nil
|
|
12
|
+
value ||= value
|
|
13
|
+
html_class ||= 'form-control'
|
|
14
|
+
typeahead ||= nil # for available options see tag_input.js.coffee
|
|
15
|
+
options ||= {}
|
|
16
|
+
html_options = options.merge({
|
|
17
|
+
class: html_class,
|
|
18
|
+
data: {role: 'tagsinput', typeahead: typeahead}
|
|
19
|
+
})
|
|
20
|
+
html_options[:value] = value unless value.nil?
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
- if form
|
|
24
|
+
= form.text_field attribute, html_options
|
|
25
|
+
- else
|
|
26
|
+
= text_field_tag attribute, value, html_options
|
|
@@ -3,7 +3,7 @@ ruby:
|
|
|
3
3
|
attribute ||= nil
|
|
4
4
|
html_class ||= 'form-control'
|
|
5
5
|
value ||= nil
|
|
6
|
-
embedded_image_url ||=
|
|
6
|
+
embedded_image_url ||= wcms_components_embedded_images_url
|
|
7
7
|
|
|
8
8
|
# Set default value
|
|
9
9
|
# We have to do it this way in case value is an empty string (as opposed to nil).
|
|
@@ -19,7 +19,9 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
spec.require_paths = ["lib"]
|
|
20
20
|
|
|
21
21
|
spec.add_dependency "ace-rails-ap", "~> 3.0"
|
|
22
|
+
spec.add_dependency "buweb_content_models", ">= 0.82"
|
|
22
23
|
spec.add_dependency "coffee-rails", ">= 4.0"
|
|
24
|
+
spec.add_dependency "pundit", "~> 0.3"
|
|
23
25
|
spec.add_dependency "sass-rails", ">= 4.0"
|
|
24
26
|
spec.add_dependency "slim", ">= 2.0"
|
|
25
27
|
spec.add_development_dependency "bundler", "~> 1.3"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Rails.application.routes.draw do
|
|
2
|
+
|
|
3
|
+
namespace :wcms_components do
|
|
4
|
+
resources :embedded_images, only: [:create], defaults: { format: 'json' }
|
|
5
|
+
|
|
6
|
+
# "people#index" is used for search purposes
|
|
7
|
+
resources :people, only: [:index], defaults: { format: 'json' }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# this is just a convenience to create a named route to rack-cas' logout
|
|
11
|
+
get '/logout' => -> env { [200, { 'Content-Type' => 'text/html' }, ['Rack::CAS should have caught this']] }, as: :logout
|
|
12
|
+
|
|
13
|
+
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require "biola_wcms_components/version"
|
|
2
2
|
require "biola_wcms_components/engine" if defined?(::Rails)
|
|
3
3
|
require "ace-rails-ap"
|
|
4
|
+
require "buweb_content_models"
|
|
5
|
+
require "pundit"
|
|
4
6
|
require "coffee-rails"
|
|
5
7
|
require "sass-rails"
|
|
6
8
|
require "slim"
|
|
@@ -18,5 +20,6 @@ module BiolaWcmsComponents
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
|
|
23
|
+
autoload :CasAuthentication, 'components/cas_authentication'
|
|
21
24
|
autoload :MenuBlock, 'components/menu_block'
|
|
22
25
|
autoload :PresentationDataEditor, 'components/presentation_data_editor'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
class CasAuthentication
|
|
2
|
+
def initialize(session)
|
|
3
|
+
@session = session
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def user
|
|
7
|
+
if username.present?
|
|
8
|
+
@user ||= User.find_or_initialize_by(username: username)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def perform
|
|
13
|
+
if authenticated?
|
|
14
|
+
true
|
|
15
|
+
elsif present?
|
|
16
|
+
if new_user?
|
|
17
|
+
if create_user!
|
|
18
|
+
authenticate!
|
|
19
|
+
end
|
|
20
|
+
elsif unauthenticated?
|
|
21
|
+
authenticate!
|
|
22
|
+
update_extra_attributes!
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :session
|
|
30
|
+
|
|
31
|
+
def present?
|
|
32
|
+
session['cas'].present?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def new_user?
|
|
36
|
+
!!user.try(:new_record?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def authenticated?
|
|
40
|
+
session[:username].present?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def unauthenticated?
|
|
44
|
+
!authenticated?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def authenticate!
|
|
48
|
+
session[:username] = username
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def update_extra_attributes!
|
|
52
|
+
user.biola_id = extra_attr(:employeeId) if extra_attr_has_key?(:employeeId)
|
|
53
|
+
user.first_name = extra_attr(:eduPersonNickname) if extra_attr_has_key?(:eduPersonNickname)
|
|
54
|
+
user.last_name = extra_attr(:sn) if extra_attr_has_key?(:sn)
|
|
55
|
+
user.email = extra_attr(:mail) if extra_attr_has_key?(:mail)
|
|
56
|
+
user.photo_url = extra_attr(:url).gsub('.jpg', '_large.jpg') if extra_attr_has_key?(:url)
|
|
57
|
+
user.entitlements = extra_attrs(:eduPersonEntitlement) if extra_attr_has_key?(:eduPersonEntitlement)
|
|
58
|
+
user.affiliations = extra_attrs(:eduPersonAffiliation) if extra_attr_has_key?(:eduPersonAffiliation)
|
|
59
|
+
user.save
|
|
60
|
+
end
|
|
61
|
+
alias :create_user! :update_extra_attributes!
|
|
62
|
+
|
|
63
|
+
def username
|
|
64
|
+
session[:username] || attrs['user']
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def attrs
|
|
68
|
+
@attrs ||= (session['cas'] || {}).with_indifferent_access
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def extra_attributes
|
|
72
|
+
@extra_attributes ||= (attrs['extra_attributes'] || {}).with_indifferent_access
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extra_attr_has_key?(key)
|
|
76
|
+
extra_attributes.has_key? key
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def extra_attr(key)
|
|
80
|
+
# Many values come back as arrays but don't really need to be
|
|
81
|
+
extra_attrs(key).first
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def extra_attrs(key)
|
|
85
|
+
Array(extra_attributes[key]).map(&:to_s)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
// NOTE: There are a few places I made changes to,
|
|
2
|
+
// so consider them when updating. Search this document for the key "CHANGED"
|
|
3
|
+
//
|
|
4
|
+
// https://github.com/timschlechter/bootstrap-tagsinput
|
|
5
|
+
// Vesion: 0.4.2
|
|
6
|
+
|
|
7
|
+
(function ($) {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
var defaultOptions = {
|
|
11
|
+
tagClass: function(item) {
|
|
12
|
+
return 'label label-info';
|
|
13
|
+
},
|
|
14
|
+
itemValue: function(item) {
|
|
15
|
+
return item ? item.toString() : item;
|
|
16
|
+
},
|
|
17
|
+
itemText: function(item) {
|
|
18
|
+
return this.itemValue(item);
|
|
19
|
+
},
|
|
20
|
+
freeInput: true,
|
|
21
|
+
addOnBlur: true,
|
|
22
|
+
maxTags: undefined,
|
|
23
|
+
maxChars: undefined,
|
|
24
|
+
confirmKeys: [13, 44],
|
|
25
|
+
onTagExists: function(item, $tag) {
|
|
26
|
+
$tag.hide().fadeIn();
|
|
27
|
+
},
|
|
28
|
+
trimValue: false,
|
|
29
|
+
allowDuplicates: false
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Constructor function
|
|
34
|
+
*/
|
|
35
|
+
function TagsInput(element, options) {
|
|
36
|
+
this.itemsArray = [];
|
|
37
|
+
|
|
38
|
+
this.$element = $(element);
|
|
39
|
+
this.$element.hide();
|
|
40
|
+
|
|
41
|
+
this.isSelect = (element.tagName === 'SELECT');
|
|
42
|
+
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
|
|
43
|
+
this.objectItems = options && options.itemValue;
|
|
44
|
+
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
|
|
45
|
+
this.inputSize = Math.max(1, this.placeholderText.length);
|
|
46
|
+
|
|
47
|
+
this.$container = $('<div class="bootstrap-tagsinput"></div>');
|
|
48
|
+
this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
|
|
49
|
+
|
|
50
|
+
this.$element.after(this.$container);
|
|
51
|
+
|
|
52
|
+
// CHANGED: (RH) This is keeping the input from resizing automatically.
|
|
53
|
+
// var inputWidth = (this.inputSize < 3 ? 3 : this.inputSize) + "em";
|
|
54
|
+
// this.$input.get(0).style.cssText = "min-width: " + inputWidth + "";
|
|
55
|
+
this.build(options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
TagsInput.prototype = {
|
|
59
|
+
constructor: TagsInput,
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Adds the given item as a new tag. Pass true to dontPushVal to prevent
|
|
63
|
+
* updating the elements val()
|
|
64
|
+
*/
|
|
65
|
+
add: function(item, dontPushVal, options) {
|
|
66
|
+
var self = this;
|
|
67
|
+
|
|
68
|
+
if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
|
|
69
|
+
return;
|
|
70
|
+
|
|
71
|
+
// Ignore falsey values, except false
|
|
72
|
+
if (item !== false && !item)
|
|
73
|
+
return;
|
|
74
|
+
|
|
75
|
+
// Trim value
|
|
76
|
+
if (typeof item === "string" && self.options.trimValue) {
|
|
77
|
+
item = $.trim(item);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Throw an error when trying to add an object while the itemValue option was not set
|
|
81
|
+
if (typeof item === "object" && !self.objectItems)
|
|
82
|
+
throw("Can't add objects when itemValue option is not set");
|
|
83
|
+
|
|
84
|
+
// Ignore strings only containg whitespace
|
|
85
|
+
if (item.toString().match(/^\s*$/))
|
|
86
|
+
return;
|
|
87
|
+
|
|
88
|
+
// If SELECT but not multiple, remove current tag
|
|
89
|
+
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
|
|
90
|
+
self.remove(self.itemsArray[0]);
|
|
91
|
+
|
|
92
|
+
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
|
|
93
|
+
var items = item.split(',');
|
|
94
|
+
if (items.length > 1) {
|
|
95
|
+
for (var i = 0; i < items.length; i++) {
|
|
96
|
+
this.add(items[i], true);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!dontPushVal)
|
|
100
|
+
self.pushVal();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var itemValue = self.options.itemValue(item),
|
|
106
|
+
itemText = self.options.itemText(item),
|
|
107
|
+
tagClass = self.options.tagClass(item);
|
|
108
|
+
|
|
109
|
+
// Ignore items allready added
|
|
110
|
+
var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
|
|
111
|
+
if (existing && !self.options.allowDuplicates) {
|
|
112
|
+
// Invoke onTagExists
|
|
113
|
+
if (self.options.onTagExists) {
|
|
114
|
+
var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
|
|
115
|
+
self.options.onTagExists(item, $existingTag);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// if length greater than limit
|
|
121
|
+
if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
|
|
122
|
+
return;
|
|
123
|
+
|
|
124
|
+
// raise beforeItemAdd arg
|
|
125
|
+
var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options});
|
|
126
|
+
self.$element.trigger(beforeItemAddEvent);
|
|
127
|
+
if (beforeItemAddEvent.cancel)
|
|
128
|
+
return;
|
|
129
|
+
|
|
130
|
+
// register item in internal array and map
|
|
131
|
+
self.itemsArray.push(item);
|
|
132
|
+
|
|
133
|
+
// add a tag element
|
|
134
|
+
var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
|
|
135
|
+
$tag.data('item', item);
|
|
136
|
+
self.findInputWrapper().before($tag);
|
|
137
|
+
$tag.after(' ');
|
|
138
|
+
|
|
139
|
+
// add <option /> if item represents a value not present in one of the <select />'s options
|
|
140
|
+
if (self.isSelect && !$('option[value="' + encodeURIComponent(itemValue) + '"]',self.$element)[0]) {
|
|
141
|
+
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
|
|
142
|
+
$option.data('item', item);
|
|
143
|
+
$option.attr('value', itemValue);
|
|
144
|
+
self.$element.append($option);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!dontPushVal)
|
|
148
|
+
self.pushVal();
|
|
149
|
+
|
|
150
|
+
// Add class when reached maxTags
|
|
151
|
+
if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
|
|
152
|
+
self.$container.addClass('bootstrap-tagsinput-max');
|
|
153
|
+
|
|
154
|
+
self.$element.trigger($.Event('itemAdded', { item: item, options: options }));
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Removes the given item. Pass true to dontPushVal to prevent updating the
|
|
159
|
+
* elements val()
|
|
160
|
+
*/
|
|
161
|
+
remove: function(item, dontPushVal, options) {
|
|
162
|
+
var self = this;
|
|
163
|
+
|
|
164
|
+
if (self.objectItems) {
|
|
165
|
+
if (typeof item === "object")
|
|
166
|
+
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
|
|
167
|
+
else
|
|
168
|
+
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
|
|
169
|
+
|
|
170
|
+
item = item[item.length-1];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (item) {
|
|
174
|
+
var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options });
|
|
175
|
+
self.$element.trigger(beforeItemRemoveEvent);
|
|
176
|
+
if (beforeItemRemoveEvent.cancel)
|
|
177
|
+
return;
|
|
178
|
+
|
|
179
|
+
$('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
|
|
180
|
+
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
|
|
181
|
+
if($.inArray(item, self.itemsArray) !== -1)
|
|
182
|
+
self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!dontPushVal)
|
|
186
|
+
self.pushVal();
|
|
187
|
+
|
|
188
|
+
// Remove class when reached maxTags
|
|
189
|
+
if (self.options.maxTags > self.itemsArray.length)
|
|
190
|
+
self.$container.removeClass('bootstrap-tagsinput-max');
|
|
191
|
+
|
|
192
|
+
self.$element.trigger($.Event('itemRemoved', { item: item, options: options }));
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Removes all items
|
|
197
|
+
*/
|
|
198
|
+
removeAll: function() {
|
|
199
|
+
var self = this;
|
|
200
|
+
|
|
201
|
+
$('.tag', self.$container).remove();
|
|
202
|
+
$('option', self.$element).remove();
|
|
203
|
+
|
|
204
|
+
while(self.itemsArray.length > 0)
|
|
205
|
+
self.itemsArray.pop();
|
|
206
|
+
|
|
207
|
+
self.pushVal();
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Refreshes the tags so they match the text/value of their corresponding
|
|
212
|
+
* item.
|
|
213
|
+
*/
|
|
214
|
+
refresh: function() {
|
|
215
|
+
var self = this;
|
|
216
|
+
$('.tag', self.$container).each(function() {
|
|
217
|
+
var $tag = $(this),
|
|
218
|
+
item = $tag.data('item'),
|
|
219
|
+
itemValue = self.options.itemValue(item),
|
|
220
|
+
itemText = self.options.itemText(item),
|
|
221
|
+
tagClass = self.options.tagClass(item);
|
|
222
|
+
|
|
223
|
+
// Update tag's class and inner text
|
|
224
|
+
$tag.attr('class', null);
|
|
225
|
+
$tag.addClass('tag ' + htmlEncode(tagClass));
|
|
226
|
+
$tag.contents().filter(function() {
|
|
227
|
+
return this.nodeType == 3;
|
|
228
|
+
})[0].nodeValue = htmlEncode(itemText);
|
|
229
|
+
|
|
230
|
+
if (self.isSelect) {
|
|
231
|
+
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
|
|
232
|
+
option.attr('value', itemValue);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Returns the items added as tags
|
|
239
|
+
*/
|
|
240
|
+
items: function() {
|
|
241
|
+
return this.itemsArray;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Assembly value by retrieving the value of each item, and set it on the
|
|
246
|
+
* element.
|
|
247
|
+
*/
|
|
248
|
+
pushVal: function() {
|
|
249
|
+
var self = this,
|
|
250
|
+
val = $.map(self.items(), function(item) {
|
|
251
|
+
return self.options.itemValue(item).toString();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
self.$element.val(val, true).trigger('change');
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Initializes the tags input behaviour on the element
|
|
259
|
+
*/
|
|
260
|
+
build: function(options) {
|
|
261
|
+
var self = this;
|
|
262
|
+
|
|
263
|
+
self.options = $.extend({}, defaultOptions, options);
|
|
264
|
+
// When itemValue is set, freeInput should always be false
|
|
265
|
+
if (self.objectItems)
|
|
266
|
+
self.options.freeInput = false;
|
|
267
|
+
|
|
268
|
+
makeOptionItemFunction(self.options, 'itemValue');
|
|
269
|
+
makeOptionItemFunction(self.options, 'itemText');
|
|
270
|
+
makeOptionFunction(self.options, 'tagClass');
|
|
271
|
+
|
|
272
|
+
// CHANGED: (RH) Removed support for Typeahead Bootstrap version 2.3.2
|
|
273
|
+
|
|
274
|
+
// typeahead.js
|
|
275
|
+
if (self.options.typeaheadjs) {
|
|
276
|
+
var typeaheadjs = self.options.typeaheadjs || {};
|
|
277
|
+
|
|
278
|
+
self.$input.typeahead(null, typeaheadjs).on('typeahead:selected', $.proxy(function (obj, datum) {
|
|
279
|
+
if (typeaheadjs.valueKey)
|
|
280
|
+
self.add(datum[typeaheadjs.valueKey]);
|
|
281
|
+
else
|
|
282
|
+
self.add(datum);
|
|
283
|
+
self.$input.typeahead('val', '');
|
|
284
|
+
}, self));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
self.$container.on('click', $.proxy(function(event) {
|
|
288
|
+
if (! self.$element.attr('disabled')) {
|
|
289
|
+
self.$input.removeAttr('disabled');
|
|
290
|
+
}
|
|
291
|
+
self.$input.focus();
|
|
292
|
+
}, self));
|
|
293
|
+
|
|
294
|
+
if (self.options.addOnBlur && self.options.freeInput) {
|
|
295
|
+
self.$input.on('focusout', $.proxy(function(event) {
|
|
296
|
+
// HACK: only process on focusout when no typeahead opened, to
|
|
297
|
+
// avoid adding the typeahead text as tag
|
|
298
|
+
if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
|
|
299
|
+
self.add(self.$input.val());
|
|
300
|
+
self.$input.val('');
|
|
301
|
+
}
|
|
302
|
+
}, self));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
self.$container.on('keydown', 'input', $.proxy(function(event) {
|
|
307
|
+
var $input = $(event.target),
|
|
308
|
+
$inputWrapper = self.findInputWrapper();
|
|
309
|
+
|
|
310
|
+
if (self.$element.attr('disabled')) {
|
|
311
|
+
self.$input.attr('disabled', 'disabled');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
switch (event.which) {
|
|
316
|
+
// BACKSPACE
|
|
317
|
+
case 8:
|
|
318
|
+
if (doGetCaretPosition($input[0]) === 0) {
|
|
319
|
+
var prev = $inputWrapper.prev();
|
|
320
|
+
if (prev) {
|
|
321
|
+
self.remove(prev.data('item'));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
// DELETE
|
|
327
|
+
case 46:
|
|
328
|
+
if (doGetCaretPosition($input[0]) === 0) {
|
|
329
|
+
var next = $inputWrapper.next();
|
|
330
|
+
if (next) {
|
|
331
|
+
self.remove(next.data('item'));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
// LEFT ARROW
|
|
337
|
+
case 37:
|
|
338
|
+
// Try to move the input before the previous tag
|
|
339
|
+
var $prevTag = $inputWrapper.prev();
|
|
340
|
+
if ($input.val().length === 0 && $prevTag[0]) {
|
|
341
|
+
$prevTag.before($inputWrapper);
|
|
342
|
+
$input.focus();
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
// RIGHT ARROW
|
|
346
|
+
case 39:
|
|
347
|
+
// Try to move the input after the next tag
|
|
348
|
+
var $nextTag = $inputWrapper.next();
|
|
349
|
+
if ($input.val().length === 0 && $nextTag[0]) {
|
|
350
|
+
$nextTag.after($inputWrapper);
|
|
351
|
+
$input.focus();
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
// ignore
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Reset internal input's size
|
|
359
|
+
var textLength = $input.val().length,
|
|
360
|
+
wordSpace = Math.ceil(textLength / 5),
|
|
361
|
+
size = textLength + wordSpace + 1;
|
|
362
|
+
$input.attr('size', Math.max(this.inputSize, $input.val().length));
|
|
363
|
+
}, self));
|
|
364
|
+
|
|
365
|
+
self.$container.on('keypress', 'input', $.proxy(function(event) {
|
|
366
|
+
var $input = $(event.target);
|
|
367
|
+
|
|
368
|
+
if (self.$element.attr('disabled')) {
|
|
369
|
+
self.$input.attr('disabled', 'disabled');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
var text = $input.val(),
|
|
374
|
+
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
|
|
375
|
+
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
|
|
376
|
+
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
|
|
377
|
+
$input.val('');
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Reset internal input's size
|
|
382
|
+
var textLength = $input.val().length,
|
|
383
|
+
wordSpace = Math.ceil(textLength / 5),
|
|
384
|
+
size = textLength + wordSpace + 1;
|
|
385
|
+
$input.attr('size', Math.max(this.inputSize, $input.val().length));
|
|
386
|
+
}, self));
|
|
387
|
+
|
|
388
|
+
// Remove icon clicked
|
|
389
|
+
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
|
|
390
|
+
if (self.$element.attr('disabled')) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
self.remove($(event.target).closest('.tag').data('item'));
|
|
394
|
+
}, self));
|
|
395
|
+
|
|
396
|
+
// Only add existing value as tags when using strings as tags
|
|
397
|
+
if (self.options.itemValue === defaultOptions.itemValue) {
|
|
398
|
+
if (self.$element[0].tagName === 'INPUT') {
|
|
399
|
+
self.add(self.$element.val());
|
|
400
|
+
} else {
|
|
401
|
+
$('option', self.$element).each(function() {
|
|
402
|
+
self.add($(this).attr('value'), true);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Removes all tagsinput behaviour and unregsiter all event handlers
|
|
410
|
+
*/
|
|
411
|
+
destroy: function() {
|
|
412
|
+
var self = this;
|
|
413
|
+
|
|
414
|
+
// Unbind events
|
|
415
|
+
self.$container.off('keypress', 'input');
|
|
416
|
+
self.$container.off('click', '[role=remove]');
|
|
417
|
+
|
|
418
|
+
self.$container.remove();
|
|
419
|
+
self.$element.removeData('tagsinput');
|
|
420
|
+
self.$element.show();
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Sets focus on the tagsinput
|
|
425
|
+
*/
|
|
426
|
+
focus: function() {
|
|
427
|
+
this.$input.focus();
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Returns the internal input element
|
|
432
|
+
*/
|
|
433
|
+
input: function() {
|
|
434
|
+
return this.$input;
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Returns the element which is wrapped around the internal input. This
|
|
439
|
+
* is normally the $container, but typeahead.js moves the $input element.
|
|
440
|
+
*/
|
|
441
|
+
findInputWrapper: function() {
|
|
442
|
+
var elt = this.$input[0],
|
|
443
|
+
container = this.$container[0];
|
|
444
|
+
while(elt && elt.parentNode !== container)
|
|
445
|
+
elt = elt.parentNode;
|
|
446
|
+
|
|
447
|
+
return $(elt);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Register JQuery plugin
|
|
453
|
+
*/
|
|
454
|
+
$.fn.tagsinput = function(arg1, arg2, arg3) {
|
|
455
|
+
var results = [];
|
|
456
|
+
|
|
457
|
+
this.each(function() {
|
|
458
|
+
var tagsinput = $(this).data('tagsinput');
|
|
459
|
+
// Initialize a new tags input
|
|
460
|
+
if (!tagsinput) {
|
|
461
|
+
tagsinput = new TagsInput(this, arg1);
|
|
462
|
+
$(this).data('tagsinput', tagsinput);
|
|
463
|
+
results.push(tagsinput);
|
|
464
|
+
|
|
465
|
+
if (this.tagName === 'SELECT') {
|
|
466
|
+
$('option', $(this)).attr('selected', 'selected');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Init tags from $(this).val()
|
|
470
|
+
$(this).val($(this).val());
|
|
471
|
+
} else if (!arg1 && !arg2) {
|
|
472
|
+
// tagsinput already exists
|
|
473
|
+
// no function, trying to init
|
|
474
|
+
results.push(tagsinput);
|
|
475
|
+
} else if(tagsinput[arg1] !== undefined) {
|
|
476
|
+
// Invoke function on existing tags input
|
|
477
|
+
if(tagsinput[arg1].length === 3 && arg3 !== undefined){
|
|
478
|
+
var retVal = tagsinput[arg1](arg2, null, arg3);
|
|
479
|
+
}else{
|
|
480
|
+
var retVal = tagsinput[arg1](arg2);
|
|
481
|
+
}
|
|
482
|
+
if (retVal !== undefined)
|
|
483
|
+
results.push(retVal);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if ( typeof arg1 == 'string') {
|
|
488
|
+
// Return the results from the invoked function calls
|
|
489
|
+
return results.length > 1 ? results : results[0];
|
|
490
|
+
} else {
|
|
491
|
+
return results;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
$.fn.tagsinput.Constructor = TagsInput;
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Most options support both a string or number as well as a function as
|
|
499
|
+
* option value. This function makes sure that the option with the given
|
|
500
|
+
* key in the given options is wrapped in a function
|
|
501
|
+
*/
|
|
502
|
+
function makeOptionItemFunction(options, key) {
|
|
503
|
+
if (typeof options[key] !== 'function') {
|
|
504
|
+
var propertyName = options[key];
|
|
505
|
+
options[key] = function(item) { return item[propertyName]; };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function makeOptionFunction(options, key) {
|
|
509
|
+
if (typeof options[key] !== 'function') {
|
|
510
|
+
var value = options[key];
|
|
511
|
+
options[key] = function() { return value; };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* HtmlEncodes the given value
|
|
516
|
+
*/
|
|
517
|
+
var htmlEncodeContainer = $('<div />');
|
|
518
|
+
function htmlEncode(value) {
|
|
519
|
+
if (value) {
|
|
520
|
+
return htmlEncodeContainer.text(value).html();
|
|
521
|
+
} else {
|
|
522
|
+
return '';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Returns the position of the caret in the given input field
|
|
528
|
+
* http://flightschool.acylt.com/devnotes/caret-position-woes/
|
|
529
|
+
*/
|
|
530
|
+
function doGetCaretPosition(oField) {
|
|
531
|
+
var iCaretPos = 0;
|
|
532
|
+
if (document.selection) {
|
|
533
|
+
oField.focus ();
|
|
534
|
+
var oSel = document.selection.createRange();
|
|
535
|
+
oSel.moveStart ('character', -oField.value.length);
|
|
536
|
+
iCaretPos = oSel.text.length;
|
|
537
|
+
} else if (oField.selectionStart || oField.selectionStart == '0') {
|
|
538
|
+
iCaretPos = oField.selectionStart;
|
|
539
|
+
}
|
|
540
|
+
return (iCaretPos);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Returns boolean indicates whether user has pressed an expected key combination.
|
|
545
|
+
* @param object keyPressEvent: JavaScript event object, refer
|
|
546
|
+
* http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
|
|
547
|
+
* @param object lookupList: expected key combinations, as in:
|
|
548
|
+
* [13, {which: 188, shiftKey: true}]
|
|
549
|
+
*/
|
|
550
|
+
function keyCombinationInList(keyPressEvent, lookupList) {
|
|
551
|
+
var found = false;
|
|
552
|
+
$.each(lookupList, function (index, keyCombination) {
|
|
553
|
+
if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
|
|
554
|
+
found = true;
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (keyPressEvent.which === keyCombination.which) {
|
|
559
|
+
var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
|
|
560
|
+
shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
|
|
561
|
+
ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
|
|
562
|
+
if (alt && shift && ctrl) {
|
|
563
|
+
found = true;
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
return found;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Initialize tagsinput behaviour on inputs and selects which have
|
|
574
|
+
* data-role=tagsinput
|
|
575
|
+
*/
|
|
576
|
+
// CHANGED: (RH) I do this myself... see tag_input.js.cofee
|
|
577
|
+
// $(function() {
|
|
578
|
+
// $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
|
|
579
|
+
// });
|
|
580
|
+
})(window.jQuery);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
.bootstrap-tagsinput {
|
|
2
|
+
background-color: #fff;
|
|
3
|
+
border: 1px solid #ccc;
|
|
4
|
+
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
|
5
|
+
display: block;
|
|
6
|
+
padding: 4px 6px;
|
|
7
|
+
color: #555;
|
|
8
|
+
vertical-align: middle;
|
|
9
|
+
border-radius: 4px;
|
|
10
|
+
max-width: 100%;
|
|
11
|
+
line-height: 22px;
|
|
12
|
+
cursor: text;
|
|
13
|
+
}
|
|
14
|
+
.bootstrap-tagsinput input {
|
|
15
|
+
border: none;
|
|
16
|
+
box-shadow: none;
|
|
17
|
+
outline: none;
|
|
18
|
+
background-color: transparent;
|
|
19
|
+
padding: 0 6px;
|
|
20
|
+
margin: 0;
|
|
21
|
+
width: auto !important;
|
|
22
|
+
max-width: inherit;
|
|
23
|
+
}
|
|
24
|
+
.bootstrap-tagsinput.form-control input::-moz-placeholder {
|
|
25
|
+
color: #777;
|
|
26
|
+
opacity: 1;
|
|
27
|
+
}
|
|
28
|
+
.bootstrap-tagsinput.form-control input:-ms-input-placeholder {
|
|
29
|
+
color: #777;
|
|
30
|
+
}
|
|
31
|
+
.bootstrap-tagsinput.form-control input::-webkit-input-placeholder {
|
|
32
|
+
color: #777;
|
|
33
|
+
}
|
|
34
|
+
.bootstrap-tagsinput input:focus {
|
|
35
|
+
border: none;
|
|
36
|
+
box-shadow: none;
|
|
37
|
+
}
|
|
38
|
+
.bootstrap-tagsinput .tag {
|
|
39
|
+
margin-right: 2px;
|
|
40
|
+
color: white;
|
|
41
|
+
}
|
|
42
|
+
.bootstrap-tagsinput .tag [data-role="remove"] {
|
|
43
|
+
margin-left: 8px;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
.bootstrap-tagsinput .tag [data-role="remove"]:after {
|
|
47
|
+
content: "x";
|
|
48
|
+
padding: 0px 2px;
|
|
49
|
+
}
|
|
50
|
+
.bootstrap-tagsinput .tag [data-role="remove"]:hover {
|
|
51
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
52
|
+
}
|
|
53
|
+
.bootstrap-tagsinput .tag [data-role="remove"]:hover:active {
|
|
54
|
+
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
|
55
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.typeahead, .tt-query, .tt-hint {
|
|
2
|
+
width: 396px;
|
|
3
|
+
height: 34px;
|
|
4
|
+
padding: 8px 12px;
|
|
5
|
+
font-size: 15px;
|
|
6
|
+
line-height: 15px;
|
|
7
|
+
border: 1px solid #ccc;
|
|
8
|
+
-webkit-border-radius: 4px;
|
|
9
|
+
-moz-border-radius: 4px;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
outline: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.tt-query {
|
|
15
|
+
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
|
16
|
+
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
|
17
|
+
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.tt-hint {
|
|
21
|
+
color: #999
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.tt-dropdown-menu {
|
|
25
|
+
text-align: left;
|
|
26
|
+
width: 422px;
|
|
27
|
+
margin-top: 12px;
|
|
28
|
+
padding: 8px 0;
|
|
29
|
+
background-color: #fff;
|
|
30
|
+
border: 1px solid #ccc;
|
|
31
|
+
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
32
|
+
-webkit-border-radius: 4px;
|
|
33
|
+
-moz-border-radius: 4px;
|
|
34
|
+
border-radius: 4px;
|
|
35
|
+
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
|
36
|
+
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
|
37
|
+
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.tt-suggestion, .tt-empty-message {
|
|
41
|
+
padding: 3px 20px;
|
|
42
|
+
font-size: 16px;
|
|
43
|
+
line-height: 24px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.tt-suggestion.tt-cursor {
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
color: #fff;
|
|
49
|
+
background-color: #0097cf;
|
|
50
|
+
}
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: biola_wcms_components
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Hall
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2015-02-
|
|
11
|
+
date: 2015-02-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ace-rails-ap
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '3.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: buweb_content_models
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.82'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.82'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: coffee-rails
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,6 +52,20 @@ dependencies:
|
|
|
38
52
|
- - ">="
|
|
39
53
|
- !ruby/object:Gem::Version
|
|
40
54
|
version: '4.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: pundit
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.3'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.3'
|
|
41
69
|
- !ruby/object:Gem::Dependency
|
|
42
70
|
name: sass-rails
|
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -112,16 +140,21 @@ files:
|
|
|
112
140
|
- app/assets/javascripts/components/forms/json_editor.js.coffee
|
|
113
141
|
- app/assets/javascripts/components/forms/person_lookup.js.coffee
|
|
114
142
|
- app/assets/javascripts/components/forms/presentation_data_editor.js.coffee
|
|
143
|
+
- app/assets/javascripts/components/forms/tag_input.js.coffee
|
|
115
144
|
- app/assets/javascripts/components/forms/yaml_editor.js.coffee
|
|
116
145
|
- app/assets/javascripts/configuration/file_uploader.js.coffee
|
|
117
146
|
- app/assets/javascripts/configuration/setup_redactor.js.coffee
|
|
118
147
|
- app/assets/stylesheets/_mixins.scss
|
|
119
148
|
- app/assets/stylesheets/_settings.scss
|
|
120
|
-
- app/assets/stylesheets/biola-wcms-components.scss
|
|
149
|
+
- app/assets/stylesheets/biola-wcms-components.css.scss
|
|
121
150
|
- app/assets/stylesheets/components/alerts/_message_list.scss
|
|
122
151
|
- app/assets/stylesheets/components/forms/_person_lookup.scss
|
|
123
152
|
- app/assets/stylesheets/components/forms/_presentation_data_editor.scss
|
|
153
|
+
- app/assets/stylesheets/components/forms/_tag_input.scss
|
|
124
154
|
- app/assets/stylesheets/components/navigation/_site_nav.scss
|
|
155
|
+
- app/controllers/wcms_application_controller.rb
|
|
156
|
+
- app/controllers/wcms_components/embedded_images_controller.rb
|
|
157
|
+
- app/controllers/wcms_components/people_controller.rb
|
|
125
158
|
- app/helpers/wcms_components/alerts_helper.rb
|
|
126
159
|
- app/helpers/wcms_components/component_helper.rb
|
|
127
160
|
- app/helpers/wcms_components/navigation_helper.rb
|
|
@@ -130,6 +163,8 @@ files:
|
|
|
130
163
|
- app/views/wcms_components/forms/_person_lookup.html.slim
|
|
131
164
|
- app/views/wcms_components/forms/_presentation_data_editor.html.slim
|
|
132
165
|
- app/views/wcms_components/forms/_redactor_editor.html.slim
|
|
166
|
+
- app/views/wcms_components/forms/_related_object_tags.html.slim
|
|
167
|
+
- app/views/wcms_components/forms/_tag_input.html.slim
|
|
133
168
|
- app/views/wcms_components/forms/_text_area.html.slim
|
|
134
169
|
- app/views/wcms_components/forms/_yaml_editor.html.slim
|
|
135
170
|
- app/views/wcms_components/navigation/_page_nav.html.slim
|
|
@@ -138,17 +173,22 @@ files:
|
|
|
138
173
|
- app/views/wcms_components/shared/_embedded_image_uploader.html.slim
|
|
139
174
|
- biola_wcms_components.gemspec
|
|
140
175
|
- config/locales/en.yml
|
|
176
|
+
- config/routes.rb
|
|
141
177
|
- lib/biola_wcms_components.rb
|
|
142
178
|
- lib/biola_wcms_components/configuration.rb
|
|
143
179
|
- lib/biola_wcms_components/engine.rb
|
|
144
180
|
- lib/biola_wcms_components/version.rb
|
|
181
|
+
- lib/components/cas_authentication.rb
|
|
145
182
|
- lib/components/menu_block.rb
|
|
146
183
|
- lib/components/presentation_data_editor.rb
|
|
184
|
+
- vendor/assets/javascripts/bootstrap-tagsinput.js
|
|
147
185
|
- vendor/assets/javascripts/handlebars.js
|
|
148
186
|
- vendor/assets/javascripts/redactor.js
|
|
149
187
|
- vendor/assets/javascripts/redactor_fullscreen.js
|
|
150
188
|
- vendor/assets/javascripts/typeahead.js
|
|
189
|
+
- vendor/assets/stylesheets/bootstrap-tagsinput.css
|
|
151
190
|
- vendor/assets/stylesheets/redactor.css
|
|
191
|
+
- vendor/assets/stylesheets/typeahead.css
|
|
152
192
|
homepage: https://github.com/biola/biola-wcms-components
|
|
153
193
|
licenses:
|
|
154
194
|
- MIT
|