bullet_train-fields 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/bullet_train_fields_manifest.js +0 -0
  6. data/app/controllers/concerns/fields/boolean_support.rb +10 -0
  7. data/app/controllers/concerns/fields/controller_support.rb +7 -0
  8. data/app/controllers/concerns/fields/date_and_time_support.rb +13 -0
  9. data/app/controllers/concerns/fields/date_support.rb +12 -0
  10. data/app/controllers/concerns/fields/options_support.rb +11 -0
  11. data/app/controllers/concerns/fields/super_select_support.rb +29 -0
  12. data/app/helpers/fields/cloudinary_image_helper.rb +17 -0
  13. data/app/helpers/fields/html_editor_helper.rb +19 -0
  14. data/app/helpers/fields/phone_field_helper.rb +7 -0
  15. data/app/helpers/fields/trix_editor_helper.rb +17 -0
  16. data/app/helpers/fields_helper.rb +19 -0
  17. data/app/javascript/controllers/fields/button_toggle_controller.js +13 -0
  18. data/app/javascript/controllers/fields/cloudinary_image_controller.js +100 -0
  19. data/app/javascript/controllers/fields/color_picker_controller.js +115 -0
  20. data/app/javascript/controllers/fields/date_controller.js +147 -0
  21. data/app/javascript/controllers/fields/file_field_controller.js +87 -0
  22. data/app/javascript/controllers/fields/phone_controller.js +32 -0
  23. data/app/javascript/controllers/fields/super_select_controller.js +76 -0
  24. data/config/routes.rb +2 -0
  25. data/lib/bullet_train/fields/engine.rb +6 -0
  26. data/lib/bullet_train/fields/version.rb +5 -0
  27. data/lib/bullet_train/fields.rb +8 -0
  28. data/lib/tasks/bullet_train/fields_tasks.rake +4 -0
  29. metadata +86 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7fdef51272f0cd0ac4d760b867f51ac4b6c8d8198c11321ea726b75954752ca6
4
+ data.tar.gz: bc88415a493dcde1ce185ccd54429716fe0b61c28d87b842e2c255ecf1be70ba
5
+ SHA512:
6
+ metadata.gz: 69e1ff8c6386572480de92c77ed4ca2a1561285d4183bf27fd6eca297d006aff60b5579a6bcc2700fe7edda3b9a4470d1beb43bc3c41ce0e67d00aea9f933ab5
7
+ data.tar.gz: df518f2a4f2d6cae1fbc8bd0df7cf39076b7d8b16343285bb35c5569c8271c1c9d9e60b218faa39e465ffeb9a22402a87b7e15c71a9010216b78cd5c7e669f2b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Andrew Culver
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # BulletTrain::Fields
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "bullet_train-fields"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install bullet_train-fields
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
File without changes
@@ -0,0 +1,10 @@
1
+ module Fields::BooleanSupport
2
+ extend ActiveSupport::Concern
3
+
4
+ def assign_boolean(strong_params, attribute)
5
+ attribute = attribute.to_s
6
+ if strong_params.dig(attribute).present?
7
+ strong_params[attribute] = ActiveModel::Type::Boolean.new.cast(strong_params[attribute]) || false
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Fields::ControllerSupport
2
+ include Fields::BooleanSupport
3
+ include Fields::DateAndTimeSupport
4
+ include Fields::DateSupport
5
+ include Fields::OptionsSupport
6
+ include Fields::SuperSelectSupport
7
+ end
@@ -0,0 +1,13 @@
1
+ module Fields::DateAndTimeSupport
2
+ extend ActiveSupport::Concern
3
+
4
+ def assign_date_and_time(strong_params, attribute)
5
+ attribute = attribute.to_s
6
+ time_zone_attribute = "#{attribute}_time_zone"
7
+ if strong_params.dig(attribute).present?
8
+ time_zone = ActiveSupport::TimeZone.new(strong_params[time_zone_attribute] || current_team.time_zone)
9
+ strong_params.delete(time_zone_attribute)
10
+ strong_params[attribute] = time_zone.strptime(strong_params[attribute], t("global.formats.date_and_time"))
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module Fields::DateSupport
2
+ extend ActiveSupport::Concern
3
+
4
+ def assign_date(strong_params, attribute)
5
+ attribute = attribute.to_s
6
+ if strong_params.dig(attribute).present?
7
+ parsed_value = Chronic.parse(strong_params[attribute])
8
+ return nil unless parsed_value
9
+ strong_params[attribute] = parsed_value.to_date
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module Fields::OptionsSupport
2
+ extend ActiveSupport::Concern
3
+
4
+ def assign_checkboxes(strong_params, attribute)
5
+ attribute = attribute.to_s
6
+ if strong_params.dig(attribute).present?
7
+ # filter out the placeholder inputs that arrive along with the form submission.
8
+ strong_params[attribute] = strong_params[attribute].select(&:present?)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ module Fields::SuperSelectSupport
2
+ extend ActiveSupport::Concern
3
+
4
+ def assign_select_options(strong_params, attribute)
5
+ attribute = attribute.to_s
6
+ # We check for nil here because an empty array isn't `present?`, but we want to assign empty arrays.
7
+ if strong_params.key?(attribute) && !strong_params[attribute].nil?
8
+ # filter out the placeholder inputs that arrive along with the form submission.
9
+ strong_params[attribute] = strong_params[attribute].select(&:present?)
10
+ end
11
+ end
12
+
13
+ def create_model_if_new(id)
14
+ if id.present?
15
+ unless /^\d+$/.match?(id)
16
+ id = yield(id).id.to_s
17
+ end
18
+ end
19
+ id
20
+ end
21
+
22
+ def create_models_if_new(ids)
23
+ ids.map do |id|
24
+ create_model_if_new(id) do
25
+ yield(id)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ module Fields::CloudinaryImageHelper
2
+ def cloudinary_image_tag(cloudinary_id, image_tag_options = {}, cloudinary_image_options = {})
3
+ return nil unless cloudinary_id.present?
4
+
5
+ if image_tag_options[:width]
6
+ cloudinary_image_options[:width] ||= image_tag_options[:width] * 2
7
+ end
8
+
9
+ if image_tag_options[:height]
10
+ cloudinary_image_options[:height] ||= image_tag_options[:height] * 2
11
+ end
12
+
13
+ cloudinary_image_options[:crop] ||= :fill
14
+
15
+ image_tag cl_image_path(cloudinary_id, cloudinary_image_options), image_tag_options
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Fields::HtmlEditorHelper
2
+ TEMPORARY_REPLACEMENT = "https://temp.bullettrain.co/"
3
+ def html_sanitize(string)
4
+ return string unless string
5
+ string = sanitize(string, tags: %w[div br strong em b i del a h1 blockquote pre ul ol li], attributes: %w[href])
6
+ links_target_blank(string).html_safe
7
+ end
8
+
9
+ def links_target_blank(body)
10
+ doc = Nokogiri::HTML(body)
11
+ doc.css("a").each do |link|
12
+ link["target"] = "_blank"
13
+ # To avoid window.opener attack when target blank is used
14
+ # https://mathiasbynens.github.io/rel-noopener/
15
+ link["rel"] = "noopener"
16
+ end
17
+ doc.to_s
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ module Fields::PhoneFieldHelper
2
+ def display_phone_number(phone_number)
3
+ return nil unless phone_number.present?
4
+ phone_number_parsed = Phonelib.parse(phone_number)
5
+ phone_number_parsed.full_international.gsub(/^\+#{phone_number_parsed.country_code}/, "<span class=\"text-muted\">+#{phone_number_parsed.country_code}</span>").html_safe
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ module Fields::TrixEditorHelper
2
+ TEMPORARY_REPLACEMENT = "https://temp.bullettrain.co/"
3
+ def trix_sanitize(string)
4
+ return string unless string
5
+ # TODO this is a hack to get around the fact that rails doesn't allow us to add any acceptable protocols.
6
+ string = string.gsub("bullettrain://", TEMPORARY_REPLACEMENT)
7
+ string = sanitize(string, tags: %w[div br strong em del a h1 blockquote pre ul ol li], attributes: %w[href])
8
+ # given the limited scope of what we're doing here, this string replace should work.
9
+ # it should also use a lot less memory than nokogiri.
10
+ string = string.gsub(/<a href="#{TEMPORARY_REPLACEMENT}(.*?)\/.*?">(.*?)<\/a>/o, "<span class=\"tribute-reference tribute-\\1-reference\">\\2</span>").html_safe
11
+ trix_content(string)
12
+ end
13
+
14
+ def trix_content(body)
15
+ links_target_blank(body).html_safe
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module FieldsHelper
2
+ def current_fields_form
3
+ @_fields_helper_forms ? @_fields_helper_forms.last : nil
4
+ end
5
+
6
+ def with_field_settings(options)
7
+ @_fields_helper_forms ||= []
8
+
9
+ if options[:form]
10
+ @_fields_helper_forms << options[:form]
11
+ end
12
+
13
+ yield
14
+
15
+ if options[:form]
16
+ @_fields_helper_forms.pop
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ import { Controller } from "stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [ "shadowField" ]
5
+
6
+ clickShadowField(event) {
7
+ // we have to stop safari from doing what we originally expected.
8
+ event.preventDefault();
9
+
10
+ // then we need to manually click the hidden checkbox or radio button ourselves.
11
+ this.shadowFieldTarget.click()
12
+ }
13
+ }
@@ -0,0 +1,100 @@
1
+ import { Controller } from "stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [ "uploadButton", "hiddenField", "thumbnail" ]
5
+ static values = {
6
+ signaturesUrl: String,
7
+ height: Number,
8
+ width: Number,
9
+ cloudName: String,
10
+ apiKey: String,
11
+ googleApiKey: String,
12
+ urlFormat: String,
13
+ sources: String,
14
+ searchByRights: Boolean
15
+ }
16
+ static classes = [ "thumbnailShown" ]
17
+
18
+ pickImageAndUpload(event) {
19
+ // don't submit the form.
20
+ event.preventDefault()
21
+
22
+ // prepare the list of default sources a user can upload an image from.
23
+ var defaultSources = ['local', 'url', 'camera']
24
+ if (this.hasGoogleApiKeyValue) {
25
+ defaultSources.push('image_search')
26
+ }
27
+
28
+ // configure cloudinary's uploader's options.
29
+ // many of these are configurable at the point where the `shared/fields/cloudinary_image` partial is included.
30
+ var options = {
31
+ cloud_name: this.cloudNameValue,
32
+ apiKey: this.apiKeyValue,
33
+ upload_preset: this.uploadPresetValue,
34
+ upload_signature: this.getCloudinarySignature.bind(this),
35
+ multiple: false,
36
+ sources: this.hasSourcesValue ? this.sourcesValue.split(',') : defaultSources,
37
+ search_by_rights: this.hasSearchByRightsValue && this.searchByRightsValue === false ? false : true // default to true.
38
+ }
39
+
40
+ if (this.hasGoogleApiKeyValue) {
41
+ options['google_api_key'] = this.googleApiKeyValue
42
+ }
43
+
44
+ // open cloudinary's upload widget.
45
+ cloudinary.openUploadWidget(options, this.handleWidgetResponse.bind(this))
46
+ }
47
+
48
+ clearImage(event) {
49
+ // don't submit the form.
50
+ event.preventDefault()
51
+
52
+ // clear the cloudinary id.
53
+ this.hiddenFieldTarget.value = null
54
+
55
+ // remove any existing image from the button.
56
+ this.removeThumbnail()
57
+ }
58
+
59
+ getCloudinarySignature(callback, paramsToSign) {
60
+ $.ajax({
61
+ url: this.signaturesUrlValue,
62
+ type: "GET",
63
+ dataType: "text",
64
+ data: {data: paramsToSign},
65
+ complete: function() { console.log("complete") },
66
+ success: function(signature, textStatus, xhr) { callback(signature) },
67
+ error: function(xhr, status, error) { console.log(xhr, status, error) }
68
+ })
69
+ }
70
+
71
+ handleWidgetResponse(error, response) {
72
+ // after the user has successfully uploaded a single file ..
73
+ if (!error && response && response.event === "success") {
74
+ const data = response.info
75
+
76
+ // update the cloudinary id field in the form.
77
+ this.hiddenFieldTarget.value = data.public_id
78
+
79
+ // remove any existing image.
80
+ this.removeThumbnail()
81
+
82
+ // generate a new image preview url.
83
+ this.addThumbnail(this.urlFormatValue.replace('CLOUDINARY_ID', data.public_id))
84
+ }
85
+ }
86
+
87
+ addThumbnail(url) {
88
+ var $imageElement = $(`<img src="${url}" width="${this.widthValue}" height="${this.heightValue}" data-${this.identifier}-target="thumbnail" />`)
89
+ $(this.uploadButtonTarget).prepend($imageElement)
90
+
91
+ // mark the image as present.
92
+ this.uploadButtonTarget.classList.add(this.thumbnailShownClass)
93
+ }
94
+
95
+ removeThumbnail() {
96
+ if (!this.hasThumbnailTarget) { return }
97
+ this.uploadButtonTarget.removeChild(this.thumbnailTarget)
98
+ this.uploadButtonTarget.classList.remove(this.thumbnailShownClass)
99
+ }
100
+ }
@@ -0,0 +1,115 @@
1
+ import { Controller } from "stimulus"
2
+ import '@simonwep/pickr/dist/themes/monolith.min.css'
3
+
4
+ import Pickr from '@simonwep/pickr';
5
+
6
+ export default class extends Controller {
7
+ static targets = [ "colorPickerValue", "colorField", "colorInput", "userSelectedColor", "colorOptions" ];
8
+ static values = { initialColor: String }
9
+
10
+ connect() {
11
+ this.initPluginInstance()
12
+ this.colorOptions = $(this.colorOptionsTarget).find('button').map(function (_, button) { return $(button).attr('data-color'); }).get()
13
+ }
14
+
15
+ disconnect() {
16
+ this.teardownPluginInstance()
17
+ }
18
+
19
+ pickColor(event) {
20
+ event.preventDefault();
21
+
22
+ const targetEl = event.target;
23
+ const color = targetEl.dataset.color;
24
+
25
+ $(this.colorInputTarget).val(color);
26
+ $(this.colorPickerValueTarget).val(color);
27
+ $(this.userSelectedColorTarget).data('color', color);
28
+ $('.button-color').removeClass('ring-2 ring-offset-2');
29
+
30
+ this.pickr.setColor(color);
31
+
32
+ targetEl.classList.add('ring-2', 'ring-offset-2');
33
+ }
34
+
35
+ pickRandomColor(event) {
36
+ event.preventDefault();
37
+
38
+ const r = Math.floor(Math.random() * 256);
39
+ const g = Math.floor(Math.random() * 256);
40
+ const b = Math.floor(Math.random() * 256);
41
+
42
+ this.pickr.setColor(`rgb ${r} ${g} ${b}`);
43
+ const hexColor = this.pickr.getColor().toHEXA().toString();
44
+ this.pickr.setColor(hexColor);
45
+
46
+ this.showUserSelectedColor(hexColor);
47
+ }
48
+
49
+ showUserSelectedColor(color) {
50
+ $(this.colorInputTarget).val(color);
51
+ $(this.colorPickerValueTarget).val(color);
52
+
53
+ $('.button-color').removeClass('ring-2 ring-offset-2');
54
+
55
+ $(this.userSelectedColorTarget)
56
+ .addClass('ring-2')
57
+ .addClass('ring-offset-2')
58
+ .css('background-color', color)
59
+ .css('--tw-ring-color', color)
60
+ .attr('data-color', color)
61
+ .show();
62
+ }
63
+
64
+ unpickColor(event) {
65
+ event.preventDefault();
66
+ $(this.colorPickerValueTarget).val('');
67
+ $(this.colorInputTarget).val('');
68
+ $(this.userSelectedColorTarget).hide();
69
+ $('.button-color').removeClass('ring-2 ring-offset-2');
70
+ }
71
+
72
+ togglePickr(event) {
73
+ event.preventDefault();
74
+ }
75
+
76
+ initPluginInstance() {
77
+ this.pickr = Pickr.create({
78
+ el: '.btn-pickr',
79
+ theme: 'monolith',
80
+ useAsButton: true,
81
+ default: this.initialColorValue || '#1E90FF',
82
+ components: {
83
+ // Main components
84
+ preview: true,
85
+ hue: true,
86
+
87
+ // Input / output Options
88
+ interaction: {
89
+ input: true,
90
+ save: true,
91
+ },
92
+ }
93
+ });
94
+
95
+ this.pickr.on('save', (color, _instance) => {
96
+ const hexaColor = color.toHEXA().toString()
97
+ if (!this.colorOptions.includes(hexaColor)) {
98
+ this.showUserSelectedColor(hexaColor);
99
+ }
100
+ this.pickr.hide();
101
+ });
102
+
103
+ const that = this
104
+
105
+ $('input[type="text"].pcr-result').on('keydown', function (e) {
106
+ if (e.key === 'Enter') {
107
+ that.pickr.applyColor(false)
108
+ }
109
+ })
110
+ }
111
+
112
+ teardownPluginInstance() {
113
+ this.pickr.destroy()
114
+ }
115
+ }
@@ -0,0 +1,147 @@
1
+ import { Controller } from "stimulus"
2
+ import I18n from "i18n-js/index.js.erb"
3
+ require("daterangepicker/daterangepicker.css");
4
+
5
+ // requires jQuery, moment, might want to consider a vanilla JS alternative
6
+ import 'daterangepicker';
7
+
8
+ export default class extends Controller {
9
+ static targets = [ "field", "clearButton", "currentTimeZoneWrapper", "timeZoneButtons", "timeZoneSelectWrapper", "timeZoneField" ]
10
+ static values = { includeTime: Boolean, defaultTimeZones: Array }
11
+
12
+ connect() {
13
+ this.initPluginInstance()
14
+ }
15
+
16
+ disconnect() {
17
+ this.teardownPluginInstance()
18
+ }
19
+
20
+ clearDate(event) {
21
+ // don't submit the form, unless it originated from the cancel/clear button
22
+ event.preventDefault()
23
+
24
+ $(this.fieldTarget).val('')
25
+ }
26
+
27
+ applyDateToField(event, picker) {
28
+ const format = this.includeTimeValue ? 'MM/DD/YYYY h:mm A' : 'MM/DD/YYYY'
29
+ $(this.fieldTarget).val(picker.startDate.format(format))
30
+ }
31
+
32
+ showTimeZoneButtons(event) {
33
+ // don't follow the anchor
34
+ event.preventDefault()
35
+
36
+ $(this.currentTimeZoneWrapperTarget).toggleClass('hidden')
37
+ $(this.timeZoneButtonsTarget).toggleClass('hidden')
38
+ }
39
+
40
+ showTimeZoneSelectWrapper(event) {
41
+ // don't follow the anchor
42
+ event.preventDefault()
43
+
44
+ $(this.timeZoneButtonsTarget).toggleClass('hidden')
45
+
46
+ if (this.hasTimeZoneSelectWrapperTarget) {
47
+ $(this.timeZoneSelectWrapperTarget).toggleClass('hidden')
48
+ }
49
+ }
50
+
51
+ resetTimeZoneUI(e) {
52
+ e && e.preventDefault()
53
+
54
+ $(this.currentTimeZoneWrapperTarget).removeClass('hidden')
55
+ $(this.timeZoneButtonsTarget).addClass('hidden')
56
+
57
+ if (this.hasTimeZoneSelectWrapperTarget) {
58
+ $(this.timeZoneSelectWrapperTarget).addClass('hidden')
59
+ }
60
+ }
61
+
62
+ setTimeZone(event) {
63
+ // don't follow the anchor
64
+ event.preventDefault()
65
+
66
+ const currentTimeZoneEl = this.currentTimeZoneWrapperTarget.querySelector('a')
67
+ const {value} = event.target.dataset
68
+
69
+ $(this.timeZoneFieldTarget).val(value)
70
+ $(currentTimeZoneEl).text(value)
71
+
72
+ $('.time-zone-button').removeClass('button').addClass('button-alternative')
73
+ $(event.target).removeClass('button-alternative').addClass('button')
74
+
75
+ this.resetTimeZoneUI()
76
+ }
77
+
78
+ initPluginInstance() {
79
+ $(this.fieldTarget).daterangepicker({
80
+ singleDatePicker: true,
81
+ timePicker: this.includeTimeValue,
82
+ timePickerIncrement: 5,
83
+ autoUpdateInput: false,
84
+ locale: {
85
+ cancelLabel: I18n.t('fields.date_field.cancel'),
86
+ applyLabel: I18n.t('fields.date_field.apply'),
87
+ format: this.includeTimeValue ? 'MM/DD/YYYY h:mm A' : 'MM/DD/YYYY'
88
+ }
89
+ })
90
+
91
+ $(this.fieldTarget).on('apply.daterangepicker', this.applyDateToField.bind(this))
92
+ $(this.fieldTarget).on('cancel.daterangepicker', this.clearDate.bind(this))
93
+
94
+ this.pluginMainEl = this.fieldTarget
95
+ this.plugin = $(this.pluginMainEl).data('daterangepicker') // weird
96
+
97
+ // Init time zone select
98
+ if (this.includeTimeValue && this.hasTimeZoneSelectWrapperTarget) {
99
+ this.timeZoneSelect = this.timeZoneSelectWrapperTarget.querySelector('select.select2')
100
+
101
+ $(this.timeZoneSelect).select2({
102
+ width: 'style'
103
+ })
104
+
105
+ const self = this
106
+
107
+ $(this.timeZoneSelect).on('change.select2', function(event) {
108
+ const currentTimeZoneEl = self.currentTimeZoneWrapperTarget.querySelector('a')
109
+ const {value} = event.target
110
+
111
+ $(self.timeZoneFieldTarget).val(value)
112
+ $(currentTimeZoneEl).text(value)
113
+
114
+ const selectedOptionTimeZoneButton = $('.selected-option-time-zone-button')
115
+
116
+ if (self.defaultTimeZonesValue.includes(value)) {
117
+ $('.time-zone-button').removeClass('button').addClass('button-alternative')
118
+ selectedOptionTimeZoneButton.addClass('hidden').attr('hidden', true)
119
+ $(`a[data-value="${value}"`).removeClass('button-alternative').addClass('button')
120
+ } else {
121
+ // deselect any selected button
122
+ $('.time-zone-button').removeClass('button').addClass('button-alternative')
123
+
124
+ selectedOptionTimeZoneButton.text(value)
125
+ selectedOptionTimeZoneButton.attr('data-value', value).removeAttr('hidden')
126
+ selectedOptionTimeZoneButton.removeClass(['hidden', 'button-alternative']).addClass('button')
127
+ }
128
+
129
+ self.resetTimeZoneUI()
130
+ })
131
+ }
132
+ }
133
+
134
+ teardownPluginInstance() {
135
+ if (this.plugin === undefined) { return }
136
+
137
+ $(this.pluginMainEl).off('apply.daterangepicker')
138
+ $(this.pluginMainEl).off('cancel.daterangepicker')
139
+
140
+ // revert to original markup, remove any event listeners
141
+ this.plugin.remove()
142
+
143
+ if (this.includeTimeValue) {
144
+ $(this.timeZoneSelect).select2('destroy');
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,87 @@
1
+ import { Controller } from "stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = [
5
+ "fileField",
6
+ "removeFileFlag",
7
+ "downloadFileButton",
8
+ "removeFileButton",
9
+ "selectFileButton",
10
+ "progressBar",
11
+ ];
12
+
13
+ connect() {
14
+ // Add upload event listeners
15
+ const initializeListener = document.addEventListener(
16
+ "direct-upload:initialize",
17
+ (event) => {
18
+ this.selectFileButtonTarget.classList.add("hidden");
19
+ this.progressBarTarget.innerText = "0%";
20
+ this.progressBarTarget.style.width = "0%";
21
+ this.progressBarTarget.setAttribute("aria-valuenow", 0);
22
+ this.progressBarTarget.parentNode.classList.remove("hidden");
23
+ }
24
+ );
25
+
26
+ const progressListener = document.addEventListener(
27
+ "direct-upload:progress",
28
+ (event) => {
29
+ const { progress } = event.detail;
30
+ const width = `${progress.toFixed(1)}%`;
31
+
32
+ this.progressBarTarget.innerText = width;
33
+ this.progressBarTarget.setAttribute("aria-valuenow", progress);
34
+ this.progressBarTarget.style.width = width;
35
+ }
36
+ );
37
+
38
+ const errorListener = document.addEventListener(
39
+ "direct-upload:error",
40
+ (event) => {
41
+ event.preventDefault();
42
+
43
+ const { error } = event.detail;
44
+ this.progressBarTarget.innerText = error;
45
+ }
46
+ );
47
+
48
+ this.uploadListeners = {
49
+ "direct-upload:initialize": initializeListener,
50
+ "direct-upload:progress": progressListener,
51
+ "direct-upload:error": errorListener,
52
+ };
53
+ }
54
+
55
+ disconnect() {
56
+ // Teardown event listeners
57
+ for (const event in this.uploadListeners) {
58
+ document.removeEventListener(event, this.uploadListeners[event]);
59
+ }
60
+ }
61
+
62
+ uploadFile() {
63
+ this.fileFieldTarget.click();
64
+ }
65
+
66
+ removeFile() {
67
+ if (this.hasDownloadFileButtonTarget) {
68
+ this.downloadFileButtonTarget.classList.add("hidden");
69
+ }
70
+
71
+ this.removeFileButtonTarget.classList.add("hidden");
72
+ this.removeFileFlagTarget.value = true;
73
+ }
74
+
75
+ handleFileSelected() {
76
+ const statusText = this.selectFileButtonTarget.querySelector("span");
77
+ const icon = this.selectFileButtonTarget.querySelector("i");
78
+
79
+ if (this.hasDownloadFileButtonTarget) {
80
+ this.downloadFileButtonTarget.remove();
81
+ }
82
+
83
+ statusText.innerText = "Select Another File";
84
+ icon.classList.remove("ti-upload");
85
+ icon.classList.add("ti-check");
86
+ }
87
+ }
@@ -0,0 +1,32 @@
1
+ import { Controller } from "stimulus"
2
+ import 'intl-tel-input/build/css/intlTelInput.css';
3
+ import intlTelInput from 'intl-tel-input';
4
+
5
+ export default class extends Controller {
6
+ static targets = [ "field" ]
7
+
8
+ connect() {
9
+ this.initPluginInstance()
10
+ }
11
+
12
+ disconnect() {
13
+ this.teardownPluginInstance()
14
+ }
15
+
16
+ initPluginInstance() {
17
+ this.plugin = intlTelInput(this.fieldTarget, {
18
+ hiddenInput: this.fieldTarget.dataset.method,
19
+ // See `config/webpack/environment.js` for where we copy this into place.
20
+ // TODO Wish we could somehow incorporate webpacker's cache-breaking hash into this. Anyone know how?
21
+ utilsScript: "/assets/intl-tel-input/utils.js",
22
+ customContainer: "w-full"
23
+ });
24
+ }
25
+
26
+ teardownPluginInstance() {
27
+ if (this.plugin === undefined) { return }
28
+
29
+ // revert to original markup, remove any event listeners
30
+ this.plugin.destroy()
31
+ }
32
+ }
@@ -0,0 +1,76 @@
1
+ import { Controller } from "stimulus"
2
+ require("select2/dist/css/select2.min.css");
3
+ import $ from 'jquery';
4
+ import 'select2';
5
+
6
+ export default class extends Controller {
7
+ static targets = [ "select" ]
8
+ static values = {
9
+ acceptsNew: Boolean,
10
+ enableSearch: Boolean,
11
+ searchUrl: String,
12
+ }
13
+
14
+ connect() {
15
+ this.initPluginInstance()
16
+ }
17
+
18
+ disconnect() {
19
+ this.teardownPluginInstance()
20
+ }
21
+
22
+ cleanupBeforeInit() {
23
+ $(this.element).find('.select2-container--default').remove()
24
+ }
25
+
26
+ initPluginInstance() {
27
+ let options = {};
28
+
29
+ if (!this.enableSearchValue) {
30
+ options.minimumResultsForSearch = -1;
31
+ }
32
+
33
+ options.tags = this.acceptsNewValue
34
+
35
+ if (this.searchUrlValue) {
36
+ options.ajax = {
37
+ url: this.searchUrlValue,
38
+ dataType: 'json',
39
+ // We enable pagination by default here
40
+ data: function(params) {
41
+ var query = {
42
+ search: params.term,
43
+ page: params.page || 1
44
+ }
45
+ return query
46
+ }
47
+ // Any additional params go here...
48
+ }
49
+ }
50
+
51
+ options.templateResult = this.formatState;
52
+ options.templateSelection = this.formatState;
53
+ options.width = 'style';
54
+
55
+ this.cleanupBeforeInit() // in case improperly torn down
56
+ this.pluginMainEl = this.selectTarget // required because this.selectTarget is unavailable on disconnect()
57
+ $(this.pluginMainEl).select2(options);
58
+ }
59
+
60
+ teardownPluginInstance() {
61
+ if (this.pluginMainEl === undefined) { return }
62
+
63
+ // revert to original markup, remove any event listeners
64
+ $(this.pluginMainEl).select2('destroy');
65
+ }
66
+
67
+ // https://stackoverflow.com/questions/29290389/select2-add-image-icon-to-option-dynamically
68
+ formatState(opt) {
69
+ var imageUrl = $(opt.element).attr('data-image');
70
+ var imageHtml = "";
71
+ if (imageUrl) {
72
+ imageHtml = '<img src="' + imageUrl + '" /> ';
73
+ }
74
+ return $('<span>' + imageHtml + opt.text + '</span>');
75
+ }
76
+ }
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,6 @@
1
+ module BulletTrain
2
+ module Fields
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module BulletTrain
2
+ module Fields
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ require "bullet_train/fields/version"
2
+ require "bullet_train/fields/engine"
3
+
4
+ module BulletTrain
5
+ module Fields
6
+ # Your code goes here...
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :bullet_train_fields do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bullet_train-fields
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Culver
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-01-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0
27
+ description: Bullet Train Fields
28
+ email:
29
+ - andrew.culver@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/assets/config/bullet_train_fields_manifest.js
38
+ - app/controllers/concerns/fields/boolean_support.rb
39
+ - app/controllers/concerns/fields/controller_support.rb
40
+ - app/controllers/concerns/fields/date_and_time_support.rb
41
+ - app/controllers/concerns/fields/date_support.rb
42
+ - app/controllers/concerns/fields/options_support.rb
43
+ - app/controllers/concerns/fields/super_select_support.rb
44
+ - app/helpers/fields/cloudinary_image_helper.rb
45
+ - app/helpers/fields/html_editor_helper.rb
46
+ - app/helpers/fields/phone_field_helper.rb
47
+ - app/helpers/fields/trix_editor_helper.rb
48
+ - app/helpers/fields_helper.rb
49
+ - app/javascript/controllers/fields/button_toggle_controller.js
50
+ - app/javascript/controllers/fields/cloudinary_image_controller.js
51
+ - app/javascript/controllers/fields/color_picker_controller.js
52
+ - app/javascript/controllers/fields/date_controller.js
53
+ - app/javascript/controllers/fields/file_field_controller.js
54
+ - app/javascript/controllers/fields/phone_controller.js
55
+ - app/javascript/controllers/fields/super_select_controller.js
56
+ - config/routes.rb
57
+ - lib/bullet_train/fields.rb
58
+ - lib/bullet_train/fields/engine.rb
59
+ - lib/bullet_train/fields/version.rb
60
+ - lib/tasks/bullet_train/fields_tasks.rake
61
+ homepage: https://github.com/bullet-train-co/bullet_train-fields
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ homepage_uri: https://github.com/bullet-train-co/bullet_train-fields
66
+ source_code_uri: https://github.com/bullet-train-co/bullet_train-fields
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.2.22
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Bullet Train Fields
86
+ test_files: []