biola_wcms_components 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/biola-wcms-components.js.coffee +2 -0
  3. data/app/assets/javascripts/components/forms/person_lookup.js.coffee +1 -1
  4. data/app/assets/javascripts/components/forms/tag_input.js.coffee +42 -0
  5. data/app/assets/stylesheets/{biola-wcms-components.scss → biola-wcms-components.css.scss} +2 -0
  6. data/app/assets/stylesheets/components/forms/_person_lookup.scss +0 -47
  7. data/app/assets/stylesheets/components/forms/_tag_input.scss +5 -0
  8. data/app/controllers/wcms_application_controller.rb +37 -0
  9. data/app/controllers/wcms_components/embedded_images_controller.rb +20 -0
  10. data/app/controllers/wcms_components/people_controller.rb +30 -0
  11. data/app/views/wcms_components/forms/_json_editor.html.slim +1 -1
  12. data/app/views/wcms_components/forms/_person_lookup.html.slim +12 -3
  13. data/app/views/wcms_components/forms/_presentation_data_editor.html.slim +3 -4
  14. data/app/views/wcms_components/forms/_related_object_tags.html.slim +12 -0
  15. data/app/views/wcms_components/forms/_tag_input.html.slim +26 -0
  16. data/app/views/wcms_components/forms/_yaml_editor.html.slim +1 -1
  17. data/app/views/wcms_components/shared/_embedded_image_uploader.html.slim +1 -1
  18. data/biola_wcms_components.gemspec +2 -0
  19. data/config/routes.rb +13 -0
  20. data/lib/biola_wcms_components.rb +3 -0
  21. data/lib/biola_wcms_components/version.rb +1 -1
  22. data/lib/components/cas_authentication.rb +87 -0
  23. data/vendor/assets/javascripts/bootstrap-tagsinput.js +580 -0
  24. data/vendor/assets/stylesheets/bootstrap-tagsinput.css +55 -0
  25. data/vendor/assets/stylesheets/typeahead.css +50 -0
  26. metadata +43 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3564d479c782c49da3714d46d516fbc535e40cfa
4
- data.tar.gz: 19904bda0bb747bfd063e87683cdb12391ec7de3
3
+ metadata.gz: f4615d1bb2bee67a6f0a7457288fc2df6796045b
4
+ data.tar.gz: 4d1f46f67c613b239f3a940d062f494c954ec969
5
5
  SHA512:
6
- metadata.gz: bea91bf31935f7321f8a5e2aadfa5e7eceef27781eb5abbc70bbb346e5dffafc03adc46a80c2d39932e93126222e495f1a70e3e54d7003e25d2068ae65c6c287
7
- data.tar.gz: 3157ae4e247df688200788bfac820cec812a1199c5fbb43217c24e82893902b24bac5e94dba651d34369331fd93b02008e7bb77778b8fb8257b8594477203f17
6
+ metadata.gz: abc4d90dfbd41be28d68801f5d7cbcd446ac7380e70960bbb3696b0c9a381a794d0c55ed23c1a7ffd1232bdda40f070315deb83fb1ed172ff73ce77c67bdd5d4
7
+ data.tar.gz: 259e675f5fdbddb58a1ba88d4c474c0d4b00e0a98ef7e2f53f466df82248843af985c0b643a882c334afe4613f64237c11ad620b024567c430ef6da5d2efad21
@@ -7,6 +7,8 @@
7
7
  #= require handlebars
8
8
  #= require typeahead
9
9
  #
10
+ #= require bootstrap-tagsinput
11
+ #
10
12
  #= require redactor
11
13
  #= require redactor_fullscreen
12
14
  #
@@ -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('value')
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,5 +1,7 @@
1
1
  // External files
2
2
  @import "../../../vendor/assets/stylesheets/redactor";
3
+ @import "../../../vendor/assets/stylesheets/bootstrap-tagsinput";
4
+ @import "../../../vendor/assets/stylesheets/typeahead";
3
5
 
4
6
  // Mixins and settings
5
7
  @import "./settings";
@@ -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,5 @@
1
+ .bootstrap-tagsinput {
2
+
3
+ // Hide the hint with tagsinput, it doesn't show up correctly.
4
+ .tt-hint { display: none; }
5
+ }
@@ -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 ||= nil # should be the post url for creating new embedded images
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
- lookup_url ||= nil
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
- = hidden_field_tag :person_id, '', class: 'hidden-person-id'
7
- = text_field_tag :person_name, '', placeholder: 'First or last name', required: true, class: 'form-control typeahead'
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
- / embedded_image_url: create_embedded_images_url
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 ||= nil # should be the post url for creating new embedded images
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,12 @@
1
+ ruby:
2
+ form ||= nil
3
+ attribute ||= nil
4
+ value ||= value
5
+ html_class ||= 'form-control'
6
+
7
+
8
+ = wcms_component "forms/tag_input",
9
+ form: form,
10
+ attribute: :related_object_tags_string,
11
+ value: value,
12
+ html_class: html_class
@@ -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 ||= nil # should be the post url for creating new embedded images
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).
@@ -1,5 +1,5 @@
1
1
  ruby:
2
- embedded_image_url ||= nil # should be the post url for creating new embedded images
2
+ embedded_image_url ||= wcms_components_embedded_images_url
3
3
 
4
4
  - if embedded_image_url
5
5
  javascript:
@@ -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'
@@ -1,3 +1,3 @@
1
1
  module BiolaWcmsComponents
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -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.1
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-25 00:00:00.000000000 Z
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