select7 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ <div name="id" value="<%= id %>" data-action="click->select7#selectTag" class="select7-option-item">
2
+ <%= content %>
3
+ </div>
@@ -0,0 +1,4 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import debounce from "lodash.debounce"
3
+
4
+ export default class extends Select7Controller(Controller, debounce) {}
@@ -0,0 +1,5 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import debounce from "lodash.debounce"
3
+ import { Select7Controller } from "select7"
4
+
5
+ export default class extends Select7Controller(Controller, debounce) {}
@@ -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
@@ -0,0 +1,3 @@
1
+ module Select7
2
+ VERSION = "0.0.1"
3
+ end
data/lib/select7.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "select7/version"
2
+ require "select7/engine"
3
+
4
+ module Select7
5
+ end
@@ -0,0 +1,9 @@
1
+ def run_select7_install_template(path)
2
+ system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{path}"
3
+ end
4
+
5
+ namespace :select7 do
6
+ task :install do
7
+ run_select7_install_template "#{File.expand_path("../install/select7.rb", __dir__)}"
8
+ end
9
+ end
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: []