bullet_train-fields 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/config/bullet_train_fields_manifest.js +0 -0
- data/app/controllers/concerns/fields/boolean_support.rb +10 -0
- data/app/controllers/concerns/fields/controller_support.rb +7 -0
- data/app/controllers/concerns/fields/date_and_time_support.rb +13 -0
- data/app/controllers/concerns/fields/date_support.rb +12 -0
- data/app/controllers/concerns/fields/options_support.rb +11 -0
- data/app/controllers/concerns/fields/super_select_support.rb +29 -0
- data/app/helpers/fields/cloudinary_image_helper.rb +17 -0
- data/app/helpers/fields/html_editor_helper.rb +19 -0
- data/app/helpers/fields/phone_field_helper.rb +7 -0
- data/app/helpers/fields/trix_editor_helper.rb +17 -0
- data/app/helpers/fields_helper.rb +19 -0
- data/app/javascript/controllers/fields/button_toggle_controller.js +13 -0
- data/app/javascript/controllers/fields/cloudinary_image_controller.js +100 -0
- data/app/javascript/controllers/fields/color_picker_controller.js +115 -0
- data/app/javascript/controllers/fields/date_controller.js +147 -0
- data/app/javascript/controllers/fields/file_field_controller.js +87 -0
- data/app/javascript/controllers/fields/phone_controller.js +32 -0
- data/app/javascript/controllers/fields/super_select_controller.js +76 -0
- data/config/routes.rb +2 -0
- data/lib/bullet_train/fields/engine.rb +6 -0
- data/lib/bullet_train/fields/version.rb +5 -0
- data/lib/bullet_train/fields.rb +8 -0
- data/lib/tasks/bullet_train/fields_tasks.rake +4 -0
- 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
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,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
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: []
|