select7 0.0.1
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 +74 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/select7.esm.js +215 -0
- data/app/assets/javascripts/select7.js +215 -0
- data/app/assets/stylesheets/select7.css +113 -0
- data/app/helpers/select7/form_helper.rb +54 -0
- data/app/helpers/select7/tag_helper.rb +23 -0
- data/app/views/select7/_field.html.erb +50 -0
- data/app/views/select7/_item.html.erb +3 -0
- data/lib/install/app/javascript/controllers/select7_controller.js +4 -0
- data/lib/install/app/javascript/controllers/select7_esm_controller.js +5 -0
- data/lib/install/select7.rb +48 -0
- data/lib/select7/engine.rb +26 -0
- data/lib/select7/version.rb +3 -0
- data/lib/select7.rb +5 -0
- data/lib/tasks/select7_tasks.rake +9 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: de12f1b5502b7b8ff49100e092b72a00b6f143b9e51fd0ea77d07a6ee4a7a2f9
|
4
|
+
data.tar.gz: 5723178cf260813f9b26be5bacf25e5a64d48b6bab738e227a2660f497b9a5b9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0061de765dd721b9bb54aecef14e0897633ac450fa1cad1ce8b4bd15a36af8318d5dfb8929277c242e594c6560dcee3b5edc1b4dc5a2363c911feea970276443
|
7
|
+
data.tar.gz: 91793c8a4fce00d561d92ce35cc1b436e84595994348bf968cb576c96bac7c11c38670e95d7db173854e6e0787442fe243e962b56117d8de98d5cf0c2e04e6ea
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022
|
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,74 @@
|
|
1
|
+
# Select7
|
2
|
+
Multiple choices selector (similar to select2, but with rails hotwire)
|
3
|
+
![search with multiple tag](/search.PNG)
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# Gemfile
|
9
|
+
gem "rails", ">=7.0.0" # require Rails 7+
|
10
|
+
gem "stimulus-rails" # require stimulus
|
11
|
+
gem "select7"
|
12
|
+
|
13
|
+
# install
|
14
|
+
$ bundle install
|
15
|
+
$ rails select7:install
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
### Searching with multiple choices
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
<%= form_with(url: search_projects_path) do |f| %>
|
24
|
+
<%= f.select7(:tags => [:id, :name], options: Tag.all) %>
|
25
|
+
<%= f.submit %>
|
26
|
+
<% end %>
|
27
|
+
|
28
|
+
# ==> This form will submit with `params[:tag_ids]` contains all ids of the selected tags
|
29
|
+
```
|
30
|
+
|
31
|
+
### In form: one/many-to-many relationship
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
<%= form_with(model: project) do |form| %>
|
35
|
+
# ...
|
36
|
+
<%= form.select7(:tags => [:id, :name], options: Tag.all) %>
|
37
|
+
# ...
|
38
|
+
<% end %>
|
39
|
+
|
40
|
+
# ==> This form will submit with `params[:project][:tag_ids]`
|
41
|
+
def project_params
|
42
|
+
params.require(:project).permit(tag_ids: [], )
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
### Suggestion for multiple choices selector
|
47
|
+
|
48
|
+
In case there're a very large number of choices, instead of query all choices as options for select, you could use a `suggestion`:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
<%= form_with(model: project) do |form| %>
|
52
|
+
# ...
|
53
|
+
# assigned devs
|
54
|
+
<%= form.select7(:developers => [:id, :name], suggest: { url: search_developers_url(page_size: 10), format: :json }) %>
|
55
|
+
# ...
|
56
|
+
<% end %>
|
57
|
+
|
58
|
+
# this require an implementation of the suggestion
|
59
|
+
resources :developers do
|
60
|
+
get :search, on: :collection
|
61
|
+
end
|
62
|
+
|
63
|
+
class DevelopersController < ApplicationController
|
64
|
+
# ...
|
65
|
+
# suggest developers
|
66
|
+
def search
|
67
|
+
@developers = Developer.where("name like ?", "%#{params[:name]}%").first(params[:page_size].to_i)
|
68
|
+
respond_to do |format|
|
69
|
+
format.json { render json: @developers, layout: false }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
# ...
|
73
|
+
end
|
74
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
load "rails/tasks/statistics.rake"
|
4
|
+
|
5
|
+
require "bundler/gem_tasks"
|
6
|
+
|
7
|
+
|
8
|
+
require 'rake/testtask'
|
9
|
+
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
task :default => :test
|
@@ -0,0 +1,215 @@
|
|
1
|
+
export const Select7Controller = (base, debounce) =>
|
2
|
+
class extends base {
|
3
|
+
static targets = [ "selected", "input", "suggestion", "template" ]
|
4
|
+
static values = {
|
5
|
+
scope: String,
|
6
|
+
field: String,
|
7
|
+
valueAttr: String,
|
8
|
+
textAttr: String,
|
9
|
+
suggest: Object,
|
10
|
+
inputName: String,
|
11
|
+
multiple: Boolean,
|
12
|
+
nested: Boolean,
|
13
|
+
items: Array,
|
14
|
+
selectedItems: Array,
|
15
|
+
}
|
16
|
+
|
17
|
+
static formats = {
|
18
|
+
"json": "application/json",
|
19
|
+
"html": "text/html",
|
20
|
+
"turbo_stream": "text/vnd.turbo-stream.html"
|
21
|
+
}
|
22
|
+
|
23
|
+
connect() {
|
24
|
+
this.count = 0
|
25
|
+
this.timeoutId = null
|
26
|
+
this.element.addEventListener("turbo:submit-end", this.clearForm.bind(this))
|
27
|
+
if (this.hasInputTarget) {
|
28
|
+
this.inputTarget.setAttribute("autocomplete", "off")
|
29
|
+
}
|
30
|
+
|
31
|
+
this.debounceSuggest = debounce(
|
32
|
+
this.suggest.bind(this),
|
33
|
+
500,
|
34
|
+
{ 'leading': true }
|
35
|
+
)
|
36
|
+
|
37
|
+
this.selectedItems = this.hasSelectedItemsValue ? this.selectedItemsValue.map((v, n, x) => [v, n]) : []
|
38
|
+
|
39
|
+
document.addEventListener('click', this.outsideClick.bind(this))
|
40
|
+
}
|
41
|
+
|
42
|
+
disconnect() {
|
43
|
+
document.removeEventListener('click', this.outsideClick.bind(this))
|
44
|
+
}
|
45
|
+
|
46
|
+
suggest() {
|
47
|
+
if (this.suggestValue["url"]) {
|
48
|
+
this.remoteSuggest()
|
49
|
+
} else if (this.hasItemsValue) {
|
50
|
+
this.localSuggest()
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
localSuggest() {
|
55
|
+
const key = this.inputTarget.value.toLowerCase()
|
56
|
+
if (key != "") {
|
57
|
+
this.suggestionTarget.innerHTML = ""
|
58
|
+
const matchedItems = this.itemsValue.filter(([value, text, lowcaseText]) => lowcaseText.includes(key))
|
59
|
+
if (matchedItems.length > 0) {
|
60
|
+
matchedItems.forEach(([value, text, x]) => this.insertSuggestItem(value, text))
|
61
|
+
this.showSuggestion()
|
62
|
+
}
|
63
|
+
} else {
|
64
|
+
this.hideSuggestion()
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
remoteSuggest() {
|
69
|
+
const searchKey = this.inputTarget.value.replaceAll(/[^\w]/g, '')
|
70
|
+
if (searchKey.length <= 0)
|
71
|
+
return
|
72
|
+
|
73
|
+
const csrfToken = document.querySelector("[name='csrf-token']")?.content
|
74
|
+
const format = this.suggestValue["format"] || "html"
|
75
|
+
const contentType = this.suggestValue["content-type"] || this.constructor.formats[format]
|
76
|
+
const suggestQueryUrl = new URL(this.suggestValue["url"])
|
77
|
+
suggestQueryUrl.searchParams.append(this.textAttrValue, searchKey)
|
78
|
+
|
79
|
+
fetch(suggestQueryUrl, {
|
80
|
+
method: this.suggestValue["method"] || 'get',
|
81
|
+
mode: 'cors',
|
82
|
+
cache: 'no-cache',
|
83
|
+
credentials: 'same-origin',
|
84
|
+
headers: {
|
85
|
+
'Accept': contentType,
|
86
|
+
'Content-Type': contentType,
|
87
|
+
'X-CSRF-Token': csrfToken,
|
88
|
+
}
|
89
|
+
})
|
90
|
+
.then((r) => r.text())
|
91
|
+
.then((result) => {
|
92
|
+
if (result) {
|
93
|
+
this.suggestionTarget.innerHTML = ""
|
94
|
+
if (this.suggestValue["format"] == "json") {
|
95
|
+
const items = JSON.parse(result)
|
96
|
+
if (items.length > 0) {
|
97
|
+
items.forEach(item => {
|
98
|
+
this.insertSuggestItem(item[this.valueAttrValue], item[this.textAttrValue])
|
99
|
+
})
|
100
|
+
this.showSuggestion()
|
101
|
+
}
|
102
|
+
} else {
|
103
|
+
this.suggestionTarget.innerHTML = result
|
104
|
+
this.showSuggestion()
|
105
|
+
}
|
106
|
+
} else {
|
107
|
+
this.hideSuggestion()
|
108
|
+
}
|
109
|
+
})
|
110
|
+
}
|
111
|
+
|
112
|
+
insertSuggestItem(value, text) {
|
113
|
+
const displayText = this.selectedItems.find(item => item[0] == value) ? `✓ ${text}` : text
|
114
|
+
const optionItem = document.createElement("div")
|
115
|
+
optionItem.setAttribute("value", value)
|
116
|
+
optionItem.setAttribute("data-action", "click->select7#selectTag")
|
117
|
+
optionItem.setAttribute("class", "select7-option-item")
|
118
|
+
optionItem.innerText = displayText
|
119
|
+
|
120
|
+
this.suggestionTarget.appendChild(optionItem)
|
121
|
+
}
|
122
|
+
|
123
|
+
selectTag(e) {
|
124
|
+
const selectedView = e.target
|
125
|
+
const value = selectedView.getAttribute("value")
|
126
|
+
const name = this.inputNameValue.replace("[?]", `[${this.count++}]`)
|
127
|
+
|
128
|
+
if (!this.selectedItems.find(item => item[0] == value)) {
|
129
|
+
this.selectedItems.push([value, name])
|
130
|
+
|
131
|
+
const input = document.createElement("input")
|
132
|
+
input.setAttribute("type", "hidden")
|
133
|
+
input.setAttribute("value", value)
|
134
|
+
input.setAttribute("name", name)
|
135
|
+
|
136
|
+
const selectedItem = this.templateTarget.cloneNode(true)
|
137
|
+
selectedItem.appendChild(input)
|
138
|
+
selectedItem.insertAdjacentHTML("afterbegin", selectedView.innerHTML)
|
139
|
+
selectedItem.classList.remove("select7-hidden")
|
140
|
+
this.selectedTarget.appendChild(selectedItem)
|
141
|
+
|
142
|
+
this.emitChangedEvent("add", name, value)
|
143
|
+
}
|
144
|
+
|
145
|
+
this.hideSuggestion()
|
146
|
+
|
147
|
+
this.inputTarget.value = ""
|
148
|
+
if (!this.multipleValue) {
|
149
|
+
this.inputTarget.classList.add("select7-invisible")
|
150
|
+
}
|
151
|
+
this.inputTarget.focus()
|
152
|
+
}
|
153
|
+
|
154
|
+
removeTag(e) {
|
155
|
+
const removeView = e.target.parentElement
|
156
|
+
const name = removeView.getAttribute("data-remove-id")
|
157
|
+
const value = removeView.getAttribute("data-remove-value")
|
158
|
+
|
159
|
+
this.selectedItems = this.selectedItems.filter((_value, _name) => _name == name && value == _value)
|
160
|
+
|
161
|
+
if (removeView.hasAttribute("data-remove-id")) {
|
162
|
+
const input = document.createElement("input")
|
163
|
+
input.setAttribute("type", "hidden")
|
164
|
+
input.setAttribute("name", name)
|
165
|
+
input.setAttribute("value", value)
|
166
|
+
|
167
|
+
this.selectedTarget.appendChild(input)
|
168
|
+
removeView.querySelectorAll('input').forEach(v => this.selectedTarget.appendChild(v))
|
169
|
+
}
|
170
|
+
|
171
|
+
const input = removeView.querySelector('input')
|
172
|
+
this.emitChangedEvent("remove", name, value)
|
173
|
+
|
174
|
+
this.selectedTarget.removeChild(removeView)
|
175
|
+
this.inputTarget.classList.remove("select7-invisible")
|
176
|
+
}
|
177
|
+
|
178
|
+
showSuggestion() {
|
179
|
+
this.suggestionTarget.classList.remove("select7-hidden")
|
180
|
+
this.suggestionTarget.scrollTo(0, 0)
|
181
|
+
}
|
182
|
+
|
183
|
+
hideSuggestion() {
|
184
|
+
this.suggestionTarget.classList.add("select7-hidden")
|
185
|
+
}
|
186
|
+
|
187
|
+
clearForm() {
|
188
|
+
this.selectedTarget.innerHTML = ""
|
189
|
+
}
|
190
|
+
|
191
|
+
emitChangedEvent(action, name, value) {
|
192
|
+
const changedEvent = new CustomEvent('select7-changed', {
|
193
|
+
detail: {
|
194
|
+
scope: this.scopeValue,
|
195
|
+
field: this.fieldValue,
|
196
|
+
action: action,
|
197
|
+
change_value: value,
|
198
|
+
values: this.selectedItems.map(item => item[0])
|
199
|
+
}
|
200
|
+
})
|
201
|
+
window.dispatchEvent(changedEvent)
|
202
|
+
}
|
203
|
+
|
204
|
+
handleKeyUp(event) {
|
205
|
+
if (event.code == "Escape") {
|
206
|
+
this.hideSuggestion()
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
outsideClick(event) {
|
211
|
+
if (!event.composedPath().includes(this.element)) {
|
212
|
+
this.hideSuggestion()
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}
|
@@ -0,0 +1,215 @@
|
|
1
|
+
const Select7Controller = (base, debounce) =>
|
2
|
+
class extends base {
|
3
|
+
static targets = [ "selected", "input", "suggestion", "template" ]
|
4
|
+
static values = {
|
5
|
+
scope: String,
|
6
|
+
field: String,
|
7
|
+
valueAttr: String,
|
8
|
+
textAttr: String,
|
9
|
+
suggest: Object,
|
10
|
+
inputName: String,
|
11
|
+
multiple: Boolean,
|
12
|
+
nested: Boolean,
|
13
|
+
items: Array,
|
14
|
+
selectedItems: Array,
|
15
|
+
}
|
16
|
+
|
17
|
+
static formats = {
|
18
|
+
"json": "application/json",
|
19
|
+
"html": "text/html",
|
20
|
+
"turbo_stream": "text/vnd.turbo-stream.html"
|
21
|
+
}
|
22
|
+
|
23
|
+
connect() {
|
24
|
+
this.count = 0
|
25
|
+
this.timeoutId = null
|
26
|
+
this.element.addEventListener("turbo:submit-end", this.clearForm.bind(this))
|
27
|
+
if (this.hasInputTarget) {
|
28
|
+
this.inputTarget.setAttribute("autocomplete", "off")
|
29
|
+
}
|
30
|
+
|
31
|
+
this.debounceSuggest = debounce(
|
32
|
+
this.suggest.bind(this),
|
33
|
+
500,
|
34
|
+
{ 'leading': true }
|
35
|
+
)
|
36
|
+
|
37
|
+
this.selectedItems = this.hasSelectedItemsValue ? this.selectedItemsValue.map((v, n, x) => [v, n]) : []
|
38
|
+
|
39
|
+
document.addEventListener('click', this.outsideClick.bind(this))
|
40
|
+
}
|
41
|
+
|
42
|
+
disconnect() {
|
43
|
+
document.removeEventListener('click', this.outsideClick.bind(this))
|
44
|
+
}
|
45
|
+
|
46
|
+
suggest() {
|
47
|
+
if (this.suggestValue["url"]) {
|
48
|
+
this.remoteSuggest()
|
49
|
+
} else if (this.hasItemsValue) {
|
50
|
+
this.localSuggest()
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
localSuggest() {
|
55
|
+
const key = this.inputTarget.value.toLowerCase()
|
56
|
+
if (key != "") {
|
57
|
+
this.suggestionTarget.innerHTML = ""
|
58
|
+
const matchedItems = this.itemsValue.filter(([value, text, lowcaseText]) => lowcaseText.includes(key))
|
59
|
+
if (matchedItems.length > 0) {
|
60
|
+
matchedItems.forEach(([value, text, x]) => this.insertSuggestItem(value, text))
|
61
|
+
this.showSuggestion()
|
62
|
+
}
|
63
|
+
} else {
|
64
|
+
this.hideSuggestion()
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
remoteSuggest() {
|
69
|
+
const searchKey = this.inputTarget.value.replaceAll(/[^\w]/g, '')
|
70
|
+
if (searchKey.length <= 0)
|
71
|
+
return
|
72
|
+
|
73
|
+
const csrfToken = document.querySelector("[name='csrf-token']")?.content
|
74
|
+
const format = this.suggestValue["format"] || "html"
|
75
|
+
const contentType = this.suggestValue["content-type"] || this.constructor.formats[format]
|
76
|
+
const suggestQueryUrl = new URL(this.suggestValue["url"])
|
77
|
+
suggestQueryUrl.searchParams.append(this.textAttrValue, searchKey)
|
78
|
+
|
79
|
+
fetch(suggestQueryUrl, {
|
80
|
+
method: this.suggestValue["method"] || 'get',
|
81
|
+
mode: 'cors',
|
82
|
+
cache: 'no-cache',
|
83
|
+
credentials: 'same-origin',
|
84
|
+
headers: {
|
85
|
+
'Accept': contentType,
|
86
|
+
'Content-Type': contentType,
|
87
|
+
'X-CSRF-Token': csrfToken,
|
88
|
+
}
|
89
|
+
})
|
90
|
+
.then((r) => r.text())
|
91
|
+
.then((result) => {
|
92
|
+
if (result) {
|
93
|
+
this.suggestionTarget.innerHTML = ""
|
94
|
+
if (this.suggestValue["format"] == "json") {
|
95
|
+
const items = JSON.parse(result)
|
96
|
+
if (items.length > 0) {
|
97
|
+
items.forEach(item => {
|
98
|
+
this.insertSuggestItem(item[this.valueAttrValue], item[this.textAttrValue])
|
99
|
+
})
|
100
|
+
this.showSuggestion()
|
101
|
+
}
|
102
|
+
} else {
|
103
|
+
this.suggestionTarget.innerHTML = result
|
104
|
+
this.showSuggestion()
|
105
|
+
}
|
106
|
+
} else {
|
107
|
+
this.hideSuggestion()
|
108
|
+
}
|
109
|
+
})
|
110
|
+
}
|
111
|
+
|
112
|
+
insertSuggestItem(value, text) {
|
113
|
+
const displayText = this.selectedItems.find(item => item[0] == value) ? `✓ ${text}` : text
|
114
|
+
const optionItem = document.createElement("div")
|
115
|
+
optionItem.setAttribute("value", value)
|
116
|
+
optionItem.setAttribute("data-action", "click->select7#selectTag")
|
117
|
+
optionItem.setAttribute("class", "select7-option-item")
|
118
|
+
optionItem.innerText = displayText
|
119
|
+
|
120
|
+
this.suggestionTarget.appendChild(optionItem)
|
121
|
+
}
|
122
|
+
|
123
|
+
selectTag(e) {
|
124
|
+
const selectedView = e.target
|
125
|
+
const value = selectedView.getAttribute("value")
|
126
|
+
const name = this.inputNameValue.replace("[?]", `[${this.count++}]`)
|
127
|
+
|
128
|
+
if (!this.selectedItems.find(item => item[0] == value)) {
|
129
|
+
this.selectedItems.push([value, name])
|
130
|
+
|
131
|
+
const input = document.createElement("input")
|
132
|
+
input.setAttribute("type", "hidden")
|
133
|
+
input.setAttribute("value", value)
|
134
|
+
input.setAttribute("name", name)
|
135
|
+
|
136
|
+
const selectedItem = this.templateTarget.cloneNode(true)
|
137
|
+
selectedItem.appendChild(input)
|
138
|
+
selectedItem.insertAdjacentHTML("afterbegin", selectedView.innerHTML)
|
139
|
+
selectedItem.classList.remove("select7-hidden")
|
140
|
+
this.selectedTarget.appendChild(selectedItem)
|
141
|
+
|
142
|
+
this.emitChangedEvent("add", name, value)
|
143
|
+
}
|
144
|
+
|
145
|
+
this.hideSuggestion()
|
146
|
+
|
147
|
+
this.inputTarget.value = ""
|
148
|
+
if (!this.multipleValue) {
|
149
|
+
this.inputTarget.classList.add("select7-invisible")
|
150
|
+
}
|
151
|
+
this.inputTarget.focus()
|
152
|
+
}
|
153
|
+
|
154
|
+
removeTag(e) {
|
155
|
+
const removeView = e.target.parentElement
|
156
|
+
const name = removeView.getAttribute("data-remove-id")
|
157
|
+
const value = removeView.getAttribute("data-remove-value")
|
158
|
+
|
159
|
+
this.selectedItems = this.selectedItems.filter((_value, _name) => _name == name && value == _value)
|
160
|
+
|
161
|
+
if (removeView.hasAttribute("data-remove-id")) {
|
162
|
+
const input = document.createElement("input")
|
163
|
+
input.setAttribute("type", "hidden")
|
164
|
+
input.setAttribute("name", name)
|
165
|
+
input.setAttribute("value", value)
|
166
|
+
|
167
|
+
this.selectedTarget.appendChild(input)
|
168
|
+
removeView.querySelectorAll('input').forEach(v => this.selectedTarget.appendChild(v))
|
169
|
+
}
|
170
|
+
|
171
|
+
const input = removeView.querySelector('input')
|
172
|
+
this.emitChangedEvent("remove", name, value)
|
173
|
+
|
174
|
+
this.selectedTarget.removeChild(removeView)
|
175
|
+
this.inputTarget.classList.remove("select7-invisible")
|
176
|
+
}
|
177
|
+
|
178
|
+
showSuggestion() {
|
179
|
+
this.suggestionTarget.classList.remove("select7-hidden")
|
180
|
+
this.suggestionTarget.scrollTo(0, 0)
|
181
|
+
}
|
182
|
+
|
183
|
+
hideSuggestion() {
|
184
|
+
this.suggestionTarget.classList.add("select7-hidden")
|
185
|
+
}
|
186
|
+
|
187
|
+
clearForm() {
|
188
|
+
this.selectedTarget.innerHTML = ""
|
189
|
+
}
|
190
|
+
|
191
|
+
emitChangedEvent(action, name, value) {
|
192
|
+
const changedEvent = new CustomEvent('select7-changed', {
|
193
|
+
detail: {
|
194
|
+
scope: this.scopeValue,
|
195
|
+
field: this.fieldValue,
|
196
|
+
action: action,
|
197
|
+
change_value: value,
|
198
|
+
values: this.selectedItems.map(item => item[0])
|
199
|
+
}
|
200
|
+
})
|
201
|
+
window.dispatchEvent(changedEvent)
|
202
|
+
}
|
203
|
+
|
204
|
+
handleKeyUp(event) {
|
205
|
+
if (event.code == "Escape") {
|
206
|
+
this.hideSuggestion()
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
outsideClick(event) {
|
211
|
+
if (!event.composedPath().includes(this.element)) {
|
212
|
+
this.hideSuggestion()
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}
|
@@ -0,0 +1,113 @@
|
|
1
|
+
.select7-container {
|
2
|
+
position: relative;
|
3
|
+
margin-top: 1.25rem;
|
4
|
+
margin-bottom: 1.25rem;
|
5
|
+
font-size: 1.0rem;
|
6
|
+
width: 90%;
|
7
|
+
}
|
8
|
+
|
9
|
+
.select7-selected-container {
|
10
|
+
display: flex;
|
11
|
+
justify-content: flex-start;
|
12
|
+
flex-wrap: wrap;
|
13
|
+
height: fit-content;
|
14
|
+
}
|
15
|
+
|
16
|
+
.select7-selection {
|
17
|
+
margin-top: 0.5rem;
|
18
|
+
display: flex;
|
19
|
+
justify-content: flex-start;
|
20
|
+
flex-wrap: wrap;
|
21
|
+
height: fit-content;
|
22
|
+
border: solid rgb(229 231 235 / var(--tw-border-opacity));
|
23
|
+
border-width: 1px;
|
24
|
+
--tw-border-opacity: 1;
|
25
|
+
padding-top: 0.5rem;
|
26
|
+
padding-bottom: 0.5rem;
|
27
|
+
padding-left: 0.75rem;
|
28
|
+
padding-right: 0.75rem;
|
29
|
+
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
30
|
+
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
31
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
32
|
+
outline: 2px solid transparent;
|
33
|
+
outline-offset: 2px;
|
34
|
+
}
|
35
|
+
|
36
|
+
.select7-selected {
|
37
|
+
display: flex;
|
38
|
+
justify-content: flex-start;
|
39
|
+
}
|
40
|
+
|
41
|
+
.select7-input {
|
42
|
+
border-style: none;
|
43
|
+
outline: 2px solid transparent;
|
44
|
+
outline-offset: 2px;
|
45
|
+
font-size: 1.0rem;
|
46
|
+
}
|
47
|
+
|
48
|
+
.select7-suggestion {
|
49
|
+
position: absolute;
|
50
|
+
left: 0px;
|
51
|
+
z-index: 50;
|
52
|
+
height: 6rem;
|
53
|
+
width: 100%;
|
54
|
+
overflow-y: auto;
|
55
|
+
overflow-x: hidden;
|
56
|
+
margin-top: -2px;
|
57
|
+
border: solid rgb(229 231 235 / var(--tw-border-opacity));
|
58
|
+
border-width: 0 1px 2px 1px;
|
59
|
+
--tw-border-opacity: 1;
|
60
|
+
--tw-bg-opacity: 1;
|
61
|
+
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
62
|
+
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
63
|
+
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
64
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
65
|
+
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
66
|
+
}
|
67
|
+
|
68
|
+
.select7-option-item {
|
69
|
+
width: 100%;
|
70
|
+
padding-left: 0.5rem;
|
71
|
+
padding-right: 0.5rem;
|
72
|
+
padding-top: 0.25rem;
|
73
|
+
padding-bottom: 0.25rem;
|
74
|
+
}
|
75
|
+
|
76
|
+
.select7-option-item:hover {
|
77
|
+
background-color: rgb(229 231 235);
|
78
|
+
cursor: pointer;
|
79
|
+
}
|
80
|
+
|
81
|
+
.select7-selected-item {
|
82
|
+
margin-right: 0.25rem;
|
83
|
+
display: flex;
|
84
|
+
align-items: center;
|
85
|
+
justify-content: space-between;
|
86
|
+
border-radius: 0.2rem;
|
87
|
+
padding: 0.25rem;
|
88
|
+
background-color: #d9e1e1;
|
89
|
+
}
|
90
|
+
|
91
|
+
.select7-item-close {
|
92
|
+
padding: 0.25rem;
|
93
|
+
margin-left: 0.25rem;
|
94
|
+
text-align: center;
|
95
|
+
line-height: 1.0rem;
|
96
|
+
}
|
97
|
+
|
98
|
+
.select7-item-close:hover {
|
99
|
+
background-color: rgb(242, 242, 244);
|
100
|
+
cursor: pointer;
|
101
|
+
}
|
102
|
+
|
103
|
+
.select7-hidden {
|
104
|
+
display: none !important;
|
105
|
+
}
|
106
|
+
|
107
|
+
.select7-invisible {
|
108
|
+
visibility: hidden;
|
109
|
+
}
|
110
|
+
|
111
|
+
.select7-visible {
|
112
|
+
visibility: visible;
|
113
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Select7::FormHelper
|
2
|
+
class ActionView::Helpers::FormBuilder
|
3
|
+
include Select7::TagHelper
|
4
|
+
|
5
|
+
def select7(options: [], selecteds: [], suggest: {}, multiple: true, params: @template.params, **attributes)
|
6
|
+
field, (value_attr, text_attr) = attributes.first
|
7
|
+
|
8
|
+
input_name = if scope = @object_name then
|
9
|
+
"#{scope}[#{field.to_s.singularize}_#{value_attr}#{multiple ? 's' : ''}]" + (multiple ? "[]" : "")
|
10
|
+
else
|
11
|
+
"#{field.to_s.singularize}_#{value_attr}" + (multiple ? "s[]" : "")
|
12
|
+
end
|
13
|
+
|
14
|
+
selecteds = (if @object then
|
15
|
+
@object.send(field)
|
16
|
+
else
|
17
|
+
clazz = field.to_s.classify.constantize
|
18
|
+
Array(clazz.where(value_attr.to_sym => params["#{field.to_s.singularize}_#{value_attr}s"]).all)
|
19
|
+
end).map { |item|
|
20
|
+
[item.send(value_attr), item.send(text_attr)]
|
21
|
+
}
|
22
|
+
|
23
|
+
options_for_select = attributes[:options_for_select] || options.map { |item| [item.send(value_attr), item.send(text_attr)] }
|
24
|
+
|
25
|
+
select7_tag(
|
26
|
+
**attributes,
|
27
|
+
options_for_select: options_for_select,
|
28
|
+
selecteds: selecteds,
|
29
|
+
suggest: suggest,
|
30
|
+
scope: @object_name,
|
31
|
+
input_name: input_name,
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: REMOVE IF NO USECASE
|
36
|
+
# def select7_fields_for(record_name, field = "id", option_items: [], selected_items: [], suggest: {}, **attributes)
|
37
|
+
# nested_attributes = nested_attributes_association?(record_name)
|
38
|
+
# association = nested_attributes ? "#{record_name}_attributes" : record_name
|
39
|
+
# scope = "#{@object_name}[#{association}]"
|
40
|
+
# input_name = "#{scope}[?][#{field}]"
|
41
|
+
|
42
|
+
# select7_tag(
|
43
|
+
# field,
|
44
|
+
# option_items,
|
45
|
+
# selected_items: selected_items,
|
46
|
+
# suggest: suggest,
|
47
|
+
# scope: scope,
|
48
|
+
# input_name: input_name,
|
49
|
+
# nested_attributes: nested_attributes,
|
50
|
+
# **attributes
|
51
|
+
# )
|
52
|
+
# end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Select7::TagHelper
|
2
|
+
def select7_tag(options_for_select: [], selecteds: [], suggest: {}, **attributes)
|
3
|
+
field, (value_attr, text_attr) = attributes.first
|
4
|
+
options_for_select.map! {|(value, text)| [value, text, text.downcase] }
|
5
|
+
attributes.reverse_merge!(css: {}, multiple: true, nested_attributes: nil)
|
6
|
+
attributes[:input_name] ||= "#{field}" + (attributes[:multiple] ? "[]" : "")
|
7
|
+
|
8
|
+
@template.render partial: "select7/field",
|
9
|
+
locals: {
|
10
|
+
field: field,
|
11
|
+
value_attr: value_attr,
|
12
|
+
text_attr: text_attr,
|
13
|
+
selected_items: selecteds,
|
14
|
+
option_items: options_for_select,
|
15
|
+
suggest: suggest || {},
|
16
|
+
**attributes
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def select7_item_tag(id, content = nil)
|
21
|
+
render partial: "select7/item", locals: {id: id, content: content || yield.html_safe }
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<div class="<%= css[:field_container] || 'select7-container' %>"
|
2
|
+
data-controller="select7"
|
3
|
+
data-action="keyup@document->select7#handleKeyUp"
|
4
|
+
data-select7-scope-value="<%= scope %>"
|
5
|
+
data-select7-field-value="<%= field %>"
|
6
|
+
data-select7-value-attr-value="<%= value_attr %>"
|
7
|
+
data-select7-text-attr-value="<%= text_attr %>"
|
8
|
+
data-select7-input-name-value="<%= input_name %>"
|
9
|
+
data-select7-multiple-value="<%= multiple %>"
|
10
|
+
data-select7-nested-value="<%= nested_attributes.nil? %>"
|
11
|
+
data-select7-suggest-value="<%= ActiveSupport::JSON.encode(suggest) %>"
|
12
|
+
data-select7-items-value="<%= ActiveSupport::JSON.encode(option_items) %>"
|
13
|
+
data-select7-selected-items-value="<%= ActiveSupport::JSON.encode(selected_items) %>"
|
14
|
+
>
|
15
|
+
|
16
|
+
<div data-select7-target="template" class="select7-hidden <%= css[:selected_item] || 'select7-selected-item' %>">
|
17
|
+
<span class="<%= css[:selected_item_close] || 'select7-item-close' %>" data-action="click->select7#removeTag">×</span>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<div class="<%= css[:selection] || 'select7-selection' %>">
|
21
|
+
<div data-select7-target="selected" class="<%= css[:selected_container] || 'select7-selected-container' %>">
|
22
|
+
<% if multiple %>
|
23
|
+
<% selected_items.each do |id, text, _| %>
|
24
|
+
<div class="<%= css[:selected_item] || 'select7-selected-item' %>"
|
25
|
+
<% if nested_attributes %>
|
26
|
+
data-remove-id=<%= "#{scope}[#{id}][_destroy]" %>
|
27
|
+
data-remove-value="true"
|
28
|
+
<% end %>
|
29
|
+
>
|
30
|
+
|
31
|
+
<% if nested_attributes %>
|
32
|
+
<input type="hidden" name=<%= "#{scope}[#{item.id}][id]" %> value="<%= id %>">
|
33
|
+
<input type="hidden" name=<%= "#{scope}[#{item.id}][#{field}]" %> value="<%= id %>">
|
34
|
+
<% else %>
|
35
|
+
<input type="hidden" name="<%= input_name %>" value="<%= id %>">
|
36
|
+
<% end %>
|
37
|
+
|
38
|
+
<span class="<%= css[:selected_item_content] || 'select7-selected-item-content' %>"><%= text %></span>
|
39
|
+
<span class="<%= css[:selected_item_close] || 'select7-item-close' %>" data-action="click->select7#removeTag">x</span>
|
40
|
+
</div>
|
41
|
+
<% end %>
|
42
|
+
<% else %>
|
43
|
+
<span class="<%= css[:selected_item_content] || 'select7-selected-item-content' %>"><%= selected_items.first %></span>
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
<input data-select7-target="input" data-action="input->select7#debounceSuggest" class="<%= css[:input] || 'select7-input' %>" autocomplete="off" placeholder="<%= field %>">
|
47
|
+
</div>
|
48
|
+
<div data-select7-target="suggestion" class="select7-hidden <%= css[:suggestion] || 'select7-suggestion' %>">
|
49
|
+
</div>
|
50
|
+
</div>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
IMPORTMAP_PATH = Rails.root.join("config/importmap.rb")
|
2
|
+
APPLICATION_LAYOUT_PATH = Rails.root.join("app/views/layouts/application.html.erb")
|
3
|
+
|
4
|
+
say "Import Select7 Js"
|
5
|
+
|
6
|
+
if IMPORTMAP_PATH.exist?
|
7
|
+
append_to_file IMPORTMAP_PATH, %(\npin "lodash.debounce", to: "https://cdn.jsdelivr.net/npm/lodash.debounce@4.0.8/+esm", preload: true\n)
|
8
|
+
append_to_file IMPORTMAP_PATH, %(pin "select7", to: "select7.esm.js", preload: true\n)
|
9
|
+
|
10
|
+
copy_file "#{__dir__}/app/javascript/controllers/select7_esm_controller.js", "app/javascript/controllers/select7_controller.js"
|
11
|
+
|
12
|
+
elsif Rails.root.join("package.json").exist?
|
13
|
+
if APPLICATION_LAYOUT_PATH.exist?
|
14
|
+
run "yarn add lodash.debounce"
|
15
|
+
|
16
|
+
insert_into_file APPLICATION_LAYOUT_PATH.to_s, <<~ERB.indent(4), before: /\s*<\/head/
|
17
|
+
\n<%= javascript_include_tag "select7", "data-turbo-track": "reload", rel: :preload, async: true %>
|
18
|
+
ERB
|
19
|
+
|
20
|
+
append_to_file Rails.root.join("app/javascript/controllers/index.js"), <<~ERB
|
21
|
+
import Select7Controller from "./select7_controller.js"
|
22
|
+
application.register("select7", Select7Controller)
|
23
|
+
ERB
|
24
|
+
|
25
|
+
append_to_file Rails.root.join("app/javascript/application.js"), <<~ERB
|
26
|
+
ERB
|
27
|
+
|
28
|
+
copy_file "#{__dir__}/app/javascript/controllers/select7_controller.js", "app/javascript/controllers/select7_controller.js"
|
29
|
+
else
|
30
|
+
say %(Couldn't Import Select7 Js), :red
|
31
|
+
end
|
32
|
+
else
|
33
|
+
say %(You must either be running with node (package.json) or importmap-rails (config/importmap.rb) to use this gem.), :red
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# ==============
|
38
|
+
|
39
|
+
say "Import Select7 Css"
|
40
|
+
if (Rails.root.join("app/assets/stylesheets/application.css")).exist?
|
41
|
+
append_to_file "app/assets/stylesheets/application.css", %(\n@import "select7.css"\n)
|
42
|
+
elsif APPLICATION_LAYOUT_PATH.exist?
|
43
|
+
insert_into_file APPLICATION_LAYOUT_PATH.to_s, <<~ERB.indent(4), before: /^\s*<%= stylesheet_link_tag/
|
44
|
+
<%= stylesheet_link_tag "select7", "select7", "data-turbo-track": "reload" %>
|
45
|
+
ERB
|
46
|
+
else
|
47
|
+
say %(Couldn't Import Select7 Css), :red
|
48
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Select7
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace Select7
|
4
|
+
|
5
|
+
config.eager_load_namespaces << Select7
|
6
|
+
config.autoload_once_paths = %W(
|
7
|
+
#{root}/app/helpers
|
8
|
+
)
|
9
|
+
|
10
|
+
initializer "select7.assets" do |app|
|
11
|
+
if app.config.respond_to?(:assets)
|
12
|
+
Rails.application.config.assets.precompile += %w( select7.js select7.esm.js select7.css )
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# initializer "select7.importmap", after: "importmap" do |app|
|
17
|
+
# app.config.importmap.paths << Engine.root.join("config/importmap.rb")
|
18
|
+
# end
|
19
|
+
|
20
|
+
initializer "select7.helpers", before: :load_config_initializers do
|
21
|
+
ActiveSupport.on_load(:action_controller_base) do
|
22
|
+
helper Select7::Engine.helpers
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/select7.rb
ADDED
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: select7
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lam Phan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-03-04 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'
|
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'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: stimulus-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: capybara
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.26'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.26'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: selenium-webdriver
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 4.8.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 4.8.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: webdrivers
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 5.0.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 5.0.0
|
111
|
+
description: Multiple choices selector (similar to select2, but with rails hotwire)
|
112
|
+
email:
|
113
|
+
- theforestvn88@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- MIT-LICENSE
|
119
|
+
- README.md
|
120
|
+
- Rakefile
|
121
|
+
- app/assets/javascripts/select7.esm.js
|
122
|
+
- app/assets/javascripts/select7.js
|
123
|
+
- app/assets/stylesheets/select7.css
|
124
|
+
- app/helpers/select7/form_helper.rb
|
125
|
+
- app/helpers/select7/tag_helper.rb
|
126
|
+
- app/views/select7/_field.html.erb
|
127
|
+
- app/views/select7/_item.html.erb
|
128
|
+
- lib/install/app/javascript/controllers/select7_controller.js
|
129
|
+
- lib/install/app/javascript/controllers/select7_esm_controller.js
|
130
|
+
- lib/install/select7.rb
|
131
|
+
- lib/select7.rb
|
132
|
+
- lib/select7/engine.rb
|
133
|
+
- lib/select7/version.rb
|
134
|
+
- lib/tasks/select7_tasks.rake
|
135
|
+
homepage: https://github.com/theforestvn88/rails-select7.git
|
136
|
+
licenses:
|
137
|
+
- MIT
|
138
|
+
metadata:
|
139
|
+
homepage_uri: https://github.com/theforestvn88/rails-select7.git
|
140
|
+
source_code_uri: https://github.com/theforestvn88/rails-select7.git
|
141
|
+
changelog_uri: https://github.com/theforestvn88/rails-select7.git
|
142
|
+
post_install_message:
|
143
|
+
rdoc_options: []
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
152
|
+
requirements:
|
153
|
+
- - ">="
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: '0'
|
156
|
+
requirements: []
|
157
|
+
rubygems_version: 3.5.4
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: Multiple choices selector (similar to select2, but with rails hotwire)
|
161
|
+
test_files: []
|