rails_notion_like_multiselect 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -0
- data/README.md +72 -4
- data/app/javascript/rails_notion_multiselect_controller.js +66 -12
- data/lib/generators/rails_notion_like_multiselect/install/templates/rails_notion_multiselect_controller.js +66 -12
- data/lib/rails_notion_like_multiselect/helpers/multiselect_helper.rb +120 -75
- data/lib/rails_notion_like_multiselect/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0053eb09bf9bc9d733102ffc6c24d72ee8de94dcc968a8003b186621a8c57d7e
|
4
|
+
data.tar.gz: a7d3554230efc1f34df5c2b29517dff2434b38b4608a9699b2382a3b92150328
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d59b5ca1b713b4b8868f1eea413bb7d7c8652b7e882e60a092503f78d165731b8152daafd189c3ef071ac32460a03c40cdc7513cbb2c8666fd068e65465cc94b
|
7
|
+
data.tar.gz: 35071347f7a07fda25377294c66d8407dc3807fc23ff554b7cf550b08edd5e224dbb2262e25e358d88d03853133c9c88adfab3d96b5de36a82141dfbca52c406
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,68 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [0.3.0] - 2025-09-25
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Production-ready code with clean, optimized implementation
|
12
|
+
- Enhanced error handling and code organization
|
13
|
+
- Improved code style and formatting consistency
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
- **MAJOR CLEANUP:** Removed all debug logging for production use
|
17
|
+
- Optimized `extract_item_data` method with early returns and cleaner structure
|
18
|
+
- Improved code formatting and indentation throughout
|
19
|
+
- Enhanced badge creation logic with better variable organization
|
20
|
+
- Streamlined Ruby code following best practices
|
21
|
+
|
22
|
+
### Fixed
|
23
|
+
- Cleaned production JavaScript controller (removed debug console.log statements)
|
24
|
+
- Improved error handling without verbose logging
|
25
|
+
- Better performance with simplified method implementations
|
26
|
+
|
27
|
+
### Technical Improvements
|
28
|
+
- Early returns in `extract_item_data` method for better performance
|
29
|
+
- Cleaner variable assignments and code flow
|
30
|
+
- Production-optimized JavaScript controller
|
31
|
+
- Consistent code style across all files
|
32
|
+
|
33
|
+
## [0.2.1] - 2025-09-25
|
34
|
+
|
35
|
+
### Fixed
|
36
|
+
- **CRITICAL FIX:** Removed `private` visibility from `extract_item_data` method - method is now public and accessible from Rails view context
|
37
|
+
- Enhanced data type handling for strings, ActiveRecord objects, hashes, and numeric values with comprehensive fallbacks
|
38
|
+
- Added robust error handling and conditional logging for better debugging without breaking when Rails is not available
|
39
|
+
- Fixed empty badge display issue when using ActiveRecord objects and string arrays
|
40
|
+
- Improved nil handling to prevent crashes with empty collections
|
41
|
+
|
42
|
+
### Changed
|
43
|
+
- Enhanced `extract_item_data` method with better type detection and logging
|
44
|
+
- Added comprehensive comments explaining method visibility requirements for Rails views
|
45
|
+
- Improved error recovery when ActiveRecord method calls fail
|
46
|
+
|
47
|
+
### Technical Details
|
48
|
+
- **Root Cause:** The `private` keyword prevented Rails views from accessing the helper method, causing undefined method errors
|
49
|
+
- **Solution:** Made method public with enhanced error handling and logging
|
50
|
+
- **Compatibility:** Maintains full backward compatibility while fixing core functionality
|
51
|
+
|
52
|
+
## [0.2.0] - 2025-09-25
|
53
|
+
|
54
|
+
### Added
|
55
|
+
- Added `value_method` and `text_method` options for flexible attribute handling
|
56
|
+
- Support for objects with custom attribute names (e.g., `slug` instead of `id`, `title` instead of `name`)
|
57
|
+
- Enhanced documentation with examples for different data formats
|
58
|
+
|
59
|
+
### Enhanced
|
60
|
+
- Improved `extract_item_data` method to handle configurable attribute methods
|
61
|
+
- Better support for hash formats with custom keys
|
62
|
+
- Maintained backward compatibility with existing `id`/`name` defaults
|
63
|
+
|
64
|
+
### Use Cases Supported
|
65
|
+
- ActiveRecord objects with any attribute names: `value_method: :slug, text_method: :title`
|
66
|
+
- Simple string arrays that return selected strings as-is
|
67
|
+
- Hash formats with custom keys
|
68
|
+
- Mixed data formats in the same collection
|
69
|
+
|
8
70
|
## [0.1.1] - 2025-09-25
|
9
71
|
|
10
72
|
### Fixed
|
data/README.md
CHANGED
@@ -49,7 +49,7 @@ gem 'rails_notion_like_multiselect', git: 'https://github.com/pageinteract/rails
|
|
49
49
|
Or to use a specific version/tag:
|
50
50
|
|
51
51
|
```ruby
|
52
|
-
gem 'rails_notion_like_multiselect', git: 'https://github.com/pageinteract/rails_notion_like_multiselect.git', tag: 'v0.
|
52
|
+
gem 'rails_notion_like_multiselect', git: 'https://github.com/pageinteract/rails_notion_like_multiselect.git', tag: 'v0.3.0'
|
53
53
|
```
|
54
54
|
|
55
55
|
Or to use a specific branch:
|
@@ -75,9 +75,26 @@ rails generate rails_notion_like_multiselect:install
|
|
75
75
|
```
|
76
76
|
|
77
77
|
This will:
|
78
|
-
- Copy the Stimulus controller to
|
79
|
-
-
|
80
|
-
-
|
78
|
+
- Copy the Stimulus controller to `app/javascript/controllers/rails_notion_multiselect_controller.js`
|
79
|
+
- The controller is automatically registered by Stimulus auto-loading
|
80
|
+
- No additional configuration needed - it just works!
|
81
|
+
|
82
|
+
### Why the JavaScript Controller is Copied
|
83
|
+
|
84
|
+
**Modern Rails Architecture:** In Rails 7+ with importmaps, gems cannot directly serve JavaScript files through the asset pipeline. The Stimulus controller must be in your app's `app/javascript/controllers/` directory to be discovered by Stimulus's `eagerLoadControllersFrom` auto-loading mechanism.
|
85
|
+
|
86
|
+
**Gem Architecture:**
|
87
|
+
```
|
88
|
+
Gem provides:
|
89
|
+
├── 💎 Ruby helper methods (multiselect_field)
|
90
|
+
├── 🛠️ Install generator
|
91
|
+
└── 📄 JavaScript controller template
|
92
|
+
|
93
|
+
Your app receives:
|
94
|
+
├── app/javascript/controllers/
|
95
|
+
│ └── rails_notion_multiselect_controller.js (copied & production-ready)
|
96
|
+
└── Enhanced views using multiselect_field helper
|
97
|
+
```
|
81
98
|
|
82
99
|
### Prerequisites
|
83
100
|
|
@@ -201,6 +218,55 @@ For server-side creation of new items:
|
|
201
218
|
) %>
|
202
219
|
```
|
203
220
|
|
221
|
+
### With Custom Attributes
|
222
|
+
|
223
|
+
For objects with custom attribute names:
|
224
|
+
|
225
|
+
```erb
|
226
|
+
<!-- Using objects with slug/title instead of id/name -->
|
227
|
+
<%= multiselect_field(
|
228
|
+
form,
|
229
|
+
:category_slugs,
|
230
|
+
collection: @categories, # Objects with .slug and .title methods
|
231
|
+
selected: @game.categories, # Selected objects
|
232
|
+
value_method: :slug, # Use slug for the value
|
233
|
+
text_method: :title, # Use title for display text
|
234
|
+
label: "Categories"
|
235
|
+
) %>
|
236
|
+
|
237
|
+
<!-- Using hash format with custom keys -->
|
238
|
+
<%= multiselect_field(
|
239
|
+
form,
|
240
|
+
:author_ids,
|
241
|
+
collection: [
|
242
|
+
{ author_id: 1, full_name: 'John Doe' },
|
243
|
+
{ author_id: 2, full_name: 'Jane Smith' }
|
244
|
+
],
|
245
|
+
selected: [{ author_id: 1, full_name: 'John Doe' }],
|
246
|
+
value_method: :author_id, # Use author_id for the value
|
247
|
+
text_method: :full_name, # Use full_name for display text
|
248
|
+
label: "Authors"
|
249
|
+
) %>
|
250
|
+
```
|
251
|
+
|
252
|
+
### With Simple Strings
|
253
|
+
|
254
|
+
For simple string arrays:
|
255
|
+
|
256
|
+
```erb
|
257
|
+
<!-- Strings as both value and display text -->
|
258
|
+
<%= multiselect_field(
|
259
|
+
form,
|
260
|
+
:skills,
|
261
|
+
collection: ["Ruby", "JavaScript", "Python", "Go"],
|
262
|
+
selected: ["Ruby", "JavaScript"],
|
263
|
+
allow_create: true,
|
264
|
+
label: "Skills"
|
265
|
+
) %>
|
266
|
+
|
267
|
+
<!-- Selected values will be the strings themselves: ["Ruby", "JavaScript"] -->
|
268
|
+
```
|
269
|
+
|
204
270
|
## Options
|
205
271
|
|
206
272
|
| Option | Type | Default | Description |
|
@@ -215,6 +281,8 @@ For server-side creation of new items:
|
|
215
281
|
| `api_endpoint` | String | `nil` | API endpoint for server-side creation |
|
216
282
|
| `help_text` | String | `nil` | Help text below the field |
|
217
283
|
| `theme` | String | `"auto"` | Theme mode: "light", "dark", or "auto" |
|
284
|
+
| `value_method` | Symbol | `:id` | Method to call for the value/id |
|
285
|
+
| `text_method` | Symbol | `:name` | Method to call for display text |
|
218
286
|
|
219
287
|
## Keyboard Shortcuts
|
220
288
|
|
@@ -63,25 +63,71 @@ export default class extends Controller {
|
|
63
63
|
}
|
64
64
|
|
65
65
|
initializeExistingSelections() {
|
66
|
-
// Get existing
|
67
|
-
const existingInputs = this.hiddenInputsTarget.querySelectorAll('input[type="hidden"]')
|
66
|
+
// Get existing badges that were rendered by Rails
|
68
67
|
const existingBadges = this.selectedItemsTarget.querySelectorAll('[data-item-id]')
|
69
68
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
69
|
+
// Process each existing badge to extract its data
|
70
|
+
existingBadges.forEach(badge => {
|
71
|
+
const itemId = badge.dataset.itemId
|
72
|
+
if (!itemId) return
|
73
|
+
|
74
|
+
// Try multiple ways to get the item name
|
75
|
+
let nameText = ''
|
76
|
+
|
77
|
+
// Method 1: Look for child span with data-item-name attribute
|
78
|
+
const nameSpan = badge.querySelector('[data-item-name]')
|
79
|
+
if (nameSpan) {
|
80
|
+
// First try the data attribute value
|
81
|
+
nameText = nameSpan.getAttribute('data-item-name')
|
82
|
+
// If empty or "true", get the text content
|
83
|
+
if (!nameText || nameText === '' || nameText === 'true') {
|
84
|
+
nameText = nameSpan.textContent?.trim() || ''
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
// Method 2: Get text from first span child
|
89
|
+
if (!nameText) {
|
90
|
+
const firstSpan = badge.querySelector('span:first-child')
|
91
|
+
if (firstSpan) {
|
92
|
+
nameText = firstSpan.textContent?.trim() || ''
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
// Method 3: Get any text directly in the badge
|
97
|
+
if (!nameText) {
|
98
|
+
// Get all text nodes in the badge
|
99
|
+
const walker = document.createTreeWalker(
|
100
|
+
badge,
|
101
|
+
NodeFilter.SHOW_TEXT,
|
102
|
+
null,
|
103
|
+
false
|
104
|
+
)
|
105
|
+
let node
|
106
|
+
while (node = walker.nextNode()) {
|
107
|
+
const text = node.textContent?.trim()
|
108
|
+
if (text && text !== '×') {
|
109
|
+
nameText = text
|
110
|
+
break
|
79
111
|
}
|
80
112
|
}
|
81
113
|
}
|
114
|
+
|
115
|
+
// Store the item in our map
|
116
|
+
if (nameText && itemId) {
|
117
|
+
this.selectedItems.set(String(itemId), nameText)
|
118
|
+
|
119
|
+
// Ensure the badge has proper event handlers
|
120
|
+
const removeBtn = badge.querySelector('button')
|
121
|
+
if (removeBtn && !removeBtn.hasAttribute('data-action')) {
|
122
|
+
removeBtn.setAttribute('data-action', 'click->rails-notion-multiselect#handleRemove')
|
123
|
+
removeBtn.setAttribute('data-item-id', itemId)
|
124
|
+
}
|
125
|
+
}
|
82
126
|
})
|
83
127
|
|
128
|
+
// DON'T call updateDisplay() here - we want to keep the existing badges!
|
84
129
|
this.updateSelectedState()
|
130
|
+
this.updateHiddenInputs()
|
85
131
|
}
|
86
132
|
|
87
133
|
storeAllOptions() {
|
@@ -607,6 +653,14 @@ export default class extends Controller {
|
|
607
653
|
}
|
608
654
|
|
609
655
|
updateDisplay() {
|
656
|
+
// Only update if we have items in our selectedItems map
|
657
|
+
// Don't clear existing badges if selectedItems is empty (like on initial load)
|
658
|
+
if (this.selectedItems.size === 0 && this.selectedItemsTarget.querySelectorAll('[data-item-id]').length > 0) {
|
659
|
+
// We have existing badges but no items in our map yet - don't clear them!
|
660
|
+
// This happens during initialization before initializeExistingSelections runs
|
661
|
+
return
|
662
|
+
}
|
663
|
+
|
610
664
|
// Clear current display
|
611
665
|
this.selectedItemsTarget.innerHTML = ''
|
612
666
|
|
@@ -635,7 +689,7 @@ export default class extends Controller {
|
|
635
689
|
: 'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20 dark:hover:bg-gray-400/20'
|
636
690
|
|
637
691
|
span.innerHTML = `
|
638
|
-
<span data-item-name>${this.escapeHtml(itemName)}</span>
|
692
|
+
<span data-item-name="${this.escapeHtml(itemName)}">${this.escapeHtml(itemName)}</span>
|
639
693
|
<button type="button"
|
640
694
|
data-action="click->rails-notion-multiselect#handleRemove"
|
641
695
|
data-item-id="${itemId}"
|
@@ -63,25 +63,71 @@ export default class extends Controller {
|
|
63
63
|
}
|
64
64
|
|
65
65
|
initializeExistingSelections() {
|
66
|
-
// Get existing
|
67
|
-
const existingInputs = this.hiddenInputsTarget.querySelectorAll('input[type="hidden"]')
|
66
|
+
// Get existing badges that were rendered by Rails
|
68
67
|
const existingBadges = this.selectedItemsTarget.querySelectorAll('[data-item-id]')
|
69
68
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
69
|
+
// Process each existing badge to extract its data
|
70
|
+
existingBadges.forEach(badge => {
|
71
|
+
const itemId = badge.dataset.itemId
|
72
|
+
if (!itemId) return
|
73
|
+
|
74
|
+
// Try multiple ways to get the item name
|
75
|
+
let nameText = ''
|
76
|
+
|
77
|
+
// Method 1: Look for child span with data-item-name attribute
|
78
|
+
const nameSpan = badge.querySelector('[data-item-name]')
|
79
|
+
if (nameSpan) {
|
80
|
+
// First try the data attribute value
|
81
|
+
nameText = nameSpan.getAttribute('data-item-name')
|
82
|
+
// If empty or "true", get the text content
|
83
|
+
if (!nameText || nameText === '' || nameText === 'true') {
|
84
|
+
nameText = nameSpan.textContent?.trim() || ''
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
// Method 2: Get text from first span child
|
89
|
+
if (!nameText) {
|
90
|
+
const firstSpan = badge.querySelector('span:first-child')
|
91
|
+
if (firstSpan) {
|
92
|
+
nameText = firstSpan.textContent?.trim() || ''
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
// Method 3: Get any text directly in the badge
|
97
|
+
if (!nameText) {
|
98
|
+
// Get all text nodes in the badge
|
99
|
+
const walker = document.createTreeWalker(
|
100
|
+
badge,
|
101
|
+
NodeFilter.SHOW_TEXT,
|
102
|
+
null,
|
103
|
+
false
|
104
|
+
)
|
105
|
+
let node
|
106
|
+
while (node = walker.nextNode()) {
|
107
|
+
const text = node.textContent?.trim()
|
108
|
+
if (text && text !== '×') {
|
109
|
+
nameText = text
|
110
|
+
break
|
79
111
|
}
|
80
112
|
}
|
81
113
|
}
|
114
|
+
|
115
|
+
// Store the item in our map
|
116
|
+
if (nameText && itemId) {
|
117
|
+
this.selectedItems.set(String(itemId), nameText)
|
118
|
+
|
119
|
+
// Ensure the badge has proper event handlers
|
120
|
+
const removeBtn = badge.querySelector('button')
|
121
|
+
if (removeBtn && !removeBtn.hasAttribute('data-action')) {
|
122
|
+
removeBtn.setAttribute('data-action', 'click->rails-notion-multiselect#handleRemove')
|
123
|
+
removeBtn.setAttribute('data-item-id', itemId)
|
124
|
+
}
|
125
|
+
}
|
82
126
|
})
|
83
127
|
|
128
|
+
// DON'T call updateDisplay() here - we want to keep the existing badges!
|
84
129
|
this.updateSelectedState()
|
130
|
+
this.updateHiddenInputs()
|
85
131
|
}
|
86
132
|
|
87
133
|
storeAllOptions() {
|
@@ -607,6 +653,14 @@ export default class extends Controller {
|
|
607
653
|
}
|
608
654
|
|
609
655
|
updateDisplay() {
|
656
|
+
// Only update if we have items in our selectedItems map
|
657
|
+
// Don't clear existing badges if selectedItems is empty (like on initial load)
|
658
|
+
if (this.selectedItems.size === 0 && this.selectedItemsTarget.querySelectorAll('[data-item-id]').length > 0) {
|
659
|
+
// We have existing badges but no items in our map yet - don't clear them!
|
660
|
+
// This happens during initialization before initializeExistingSelections runs
|
661
|
+
return
|
662
|
+
}
|
663
|
+
|
610
664
|
// Clear current display
|
611
665
|
this.selectedItemsTarget.innerHTML = ''
|
612
666
|
|
@@ -635,7 +689,7 @@ export default class extends Controller {
|
|
635
689
|
: 'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20 dark:hover:bg-gray-400/20'
|
636
690
|
|
637
691
|
span.innerHTML = `
|
638
|
-
<span data-item-name>${this.escapeHtml(itemName)}</span>
|
692
|
+
<span data-item-name="${this.escapeHtml(itemName)}">${this.escapeHtml(itemName)}</span>
|
639
693
|
<button type="button"
|
640
694
|
data-action="click->rails-notion-multiselect#handleRemove"
|
641
695
|
data-item-id="${itemId}"
|
@@ -18,6 +18,8 @@ module RailsNotionLikeMultiselect
|
|
18
18
|
# @option options [String] :api_endpoint API endpoint for creating new items
|
19
19
|
# @option options [String] :help_text Help text to display below the field
|
20
20
|
# @option options [String] :theme Theme mode ('light', 'dark', or 'auto')
|
21
|
+
# @option options [Symbol] :value_method Method to call for the value/id (default: :id for objects, self for strings)
|
22
|
+
# @option options [Symbol] :text_method Method to call for display text (default: :name for objects, self for strings)
|
21
23
|
#
|
22
24
|
def multiselect_field(form, field, options = {})
|
23
25
|
collection = options[:collection] || []
|
@@ -30,74 +32,76 @@ module RailsNotionLikeMultiselect
|
|
30
32
|
api_endpoint = options[:api_endpoint]
|
31
33
|
help_text = options[:help_text]
|
32
34
|
theme = options[:theme] || 'auto'
|
35
|
+
value_method = options[:value_method] || :id
|
36
|
+
text_method = options[:text_method] || :name
|
33
37
|
input_name = "#{form.object_name}[#{field}][]"
|
34
38
|
|
35
39
|
# Determine theme-specific classes based on theme option
|
36
40
|
is_dark_theme = theme == 'dark'
|
37
41
|
is_light_theme = theme == 'light'
|
38
|
-
|
42
|
+
|
39
43
|
# Support both light and dark modes with proper contrast
|
40
44
|
badge_classes = if is_dark_theme
|
41
45
|
# Force dark theme styles
|
42
46
|
case badge_color
|
43
47
|
when 'green'
|
44
48
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
45
|
-
|
49
|
+
'bg-green-900/30 text-green-200 border border-green-800'
|
46
50
|
when 'purple'
|
47
51
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
48
|
-
|
52
|
+
'bg-purple-900/30 text-purple-200 border border-purple-800'
|
49
53
|
when 'yellow'
|
50
54
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
51
|
-
|
55
|
+
'bg-yellow-900/30 text-yellow-200 border border-yellow-800'
|
52
56
|
when 'red'
|
53
57
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
54
|
-
|
58
|
+
'bg-red-900/30 text-red-200 border border-red-800'
|
55
59
|
else
|
56
60
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
57
|
-
|
61
|
+
'bg-blue-900/30 text-blue-200 border border-blue-800'
|
58
62
|
end
|
59
63
|
elsif is_light_theme
|
60
64
|
# Force light theme styles
|
61
65
|
case badge_color
|
62
66
|
when 'green'
|
63
67
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
64
|
-
|
68
|
+
'bg-green-100 text-green-800 border border-green-200'
|
65
69
|
when 'purple'
|
66
70
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
67
|
-
|
71
|
+
'bg-purple-100 text-purple-800 border border-purple-200'
|
68
72
|
when 'yellow'
|
69
73
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
70
|
-
|
74
|
+
'bg-yellow-100 text-yellow-800 border border-yellow-200'
|
71
75
|
when 'red'
|
72
76
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
73
|
-
|
77
|
+
'bg-red-100 text-red-800 border border-red-200'
|
74
78
|
else
|
75
79
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
76
|
-
|
80
|
+
'bg-blue-100 text-blue-800 border border-blue-200'
|
77
81
|
end
|
78
82
|
else
|
79
83
|
# Auto mode - use Tailwind's dark: variants
|
80
84
|
case badge_color
|
81
85
|
when 'green'
|
82
86
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
83
|
-
|
84
|
-
|
87
|
+
'bg-green-100 text-green-800 border border-green-200 ' +
|
88
|
+
'dark:bg-green-900/30 dark:text-green-200 dark:border-green-800'
|
85
89
|
when 'purple'
|
86
90
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
87
|
-
|
88
|
-
|
91
|
+
'bg-purple-100 text-purple-800 border border-purple-200 ' +
|
92
|
+
'dark:bg-purple-900/30 dark:text-purple-200 dark:border-purple-800'
|
89
93
|
when 'yellow'
|
90
94
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
91
|
-
|
92
|
-
|
95
|
+
'bg-yellow-100 text-yellow-800 border border-yellow-200 ' +
|
96
|
+
'dark:bg-yellow-900/30 dark:text-yellow-200 dark:border-yellow-800'
|
93
97
|
when 'red'
|
94
98
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
95
|
-
|
96
|
-
|
99
|
+
'bg-red-100 text-red-800 border border-red-200 ' +
|
100
|
+
'dark:bg-red-900/30 dark:text-red-200 dark:border-red-800'
|
97
101
|
else
|
98
102
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
99
|
-
|
100
|
-
|
103
|
+
'bg-blue-100 text-blue-800 border border-blue-200 ' +
|
104
|
+
'dark:bg-blue-900/30 dark:text-blue-200 dark:border-blue-800'
|
101
105
|
end
|
102
106
|
end
|
103
107
|
|
@@ -127,26 +131,26 @@ module RailsNotionLikeMultiselect
|
|
127
131
|
|
128
132
|
# Input container with selected items inside
|
129
133
|
input_container_class = if is_dark_theme
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
134
|
+
'flex flex-wrap items-center gap-1.5 rounded-lg ' +
|
135
|
+
'bg-gray-900 py-2 px-3 text-sm text-white ' +
|
136
|
+
'border border-gray-700 ' +
|
137
|
+
'focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-transparent ' +
|
138
|
+
'min-h-[42px]'
|
139
|
+
elsif is_light_theme
|
140
|
+
'flex flex-wrap items-center gap-1.5 rounded-lg ' +
|
141
|
+
'bg-white py-2 px-3 text-sm text-gray-900 ' +
|
142
|
+
'border border-gray-300 ' +
|
143
|
+
'focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-transparent ' +
|
144
|
+
'min-h-[42px]'
|
145
|
+
else
|
146
|
+
'flex flex-wrap items-center gap-1.5 rounded-lg ' +
|
147
|
+
'bg-white dark:bg-gray-900 ' +
|
148
|
+
'py-2 px-3 text-sm ' +
|
149
|
+
'text-gray-900 dark:text-white ' +
|
150
|
+
'border border-gray-300 dark:border-gray-700 ' +
|
151
|
+
'focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-transparent ' +
|
152
|
+
'min-h-[42px]'
|
153
|
+
end
|
150
154
|
input_container_html = content_tag :div,
|
151
155
|
class: input_container_class,
|
152
156
|
data: { action: 'click->rails-notion-multiselect#focusInput' } do
|
@@ -155,48 +159,52 @@ module RailsNotionLikeMultiselect
|
|
155
159
|
data: { rails_notion_multiselect_target: 'selectedItems' },
|
156
160
|
class: 'flex flex-wrap gap-1.5 items-center' do
|
157
161
|
selected.map do |item|
|
158
|
-
item_id =
|
159
|
-
|
162
|
+
item_id, item_name = extract_item_data(item, value_method, text_method)
|
163
|
+
|
164
|
+
button_class = if is_dark_theme
|
165
|
+
'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-400/20'
|
166
|
+
elsif is_light_theme
|
167
|
+
'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20'
|
168
|
+
else
|
169
|
+
'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20 dark:hover:bg-gray-400/20'
|
170
|
+
end
|
160
171
|
|
161
|
-
content_tag
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
elsif is_light_theme
|
168
|
-
'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20'
|
169
|
-
else
|
170
|
-
'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20 dark:hover:bg-gray-400/20'
|
171
|
-
end
|
172
|
+
name_span = content_tag(:span, item_name, data: { item_name: item_name })
|
173
|
+
|
174
|
+
badge_html = content_tag :span,
|
175
|
+
class: badge_classes,
|
176
|
+
data: { item_id: item_id } do
|
177
|
+
name_span +
|
172
178
|
content_tag(:button,
|
173
179
|
type: 'button',
|
174
180
|
data: { action: 'click->rails-notion-multiselect#handleRemove', item_id: item_id },
|
175
181
|
class: button_class) do
|
176
|
-
content_tag(:svg,
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
182
|
+
content_tag(:svg,
|
183
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
184
|
+
viewBox: '0 0 14 14',
|
185
|
+
fill: 'none',
|
186
|
+
stroke: 'currentColor',
|
187
|
+
'stroke-width': '2',
|
188
|
+
'stroke-linecap': 'round',
|
189
|
+
'stroke-linejoin': 'round',
|
190
|
+
class: 'h-3.5 w-3.5 opacity-60 group-hover:opacity-100') do
|
185
191
|
tag.path(d: 'M4 4l6 6m0-6l-6 6')
|
186
192
|
end
|
187
193
|
end
|
188
194
|
end
|
195
|
+
|
196
|
+
badge_html
|
189
197
|
end.join.html_safe
|
190
198
|
end
|
191
199
|
|
192
200
|
# Input field
|
193
201
|
input_placeholder_class = if is_dark_theme
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
202
|
+
'placeholder-gray-500'
|
203
|
+
elsif is_light_theme
|
204
|
+
'placeholder-gray-400'
|
205
|
+
else
|
206
|
+
'placeholder-gray-400 dark:placeholder-gray-500'
|
207
|
+
end
|
200
208
|
input_html = tag.input(type: 'text',
|
201
209
|
placeholder: selected.empty? ? placeholder : '',
|
202
210
|
data: { rails_notion_multiselect_target: 'input' },
|
@@ -221,12 +229,11 @@ module RailsNotionLikeMultiselect
|
|
221
229
|
data: { rails_notion_multiselect_target: 'optionsList' },
|
222
230
|
class: 'max-h-60 overflow-auto py-1' do
|
223
231
|
collection.map do |item|
|
224
|
-
item_id =
|
225
|
-
|
226
|
-
|
227
|
-
s_id = s.respond_to?(:id) ? s.id.to_s : s.to_s
|
232
|
+
item_id, item_name = extract_item_data(item, value_method, text_method)
|
233
|
+
is_selected = selected.any? do |s|
|
234
|
+
s_id, = extract_item_data(s, value_method, text_method)
|
228
235
|
s_id == item_id
|
229
|
-
|
236
|
+
end
|
230
237
|
|
231
238
|
content_tag :div,
|
232
239
|
data: {
|
@@ -258,7 +265,7 @@ module RailsNotionLikeMultiselect
|
|
258
265
|
hidden_inputs_html = content_tag :div, data: { rails_notion_multiselect_target: 'hiddenInputs' } do
|
259
266
|
if selected.any?
|
260
267
|
selected.map do |item|
|
261
|
-
item_id =
|
268
|
+
item_id, = extract_item_data(item, value_method, text_method)
|
262
269
|
tag.input(type: 'hidden', name: input_name, value: item_id)
|
263
270
|
end.join.html_safe
|
264
271
|
else
|
@@ -283,6 +290,44 @@ module RailsNotionLikeMultiselect
|
|
283
290
|
label_html + input_container_html + dropdown_html + hidden_inputs_html + help_text_html
|
284
291
|
end
|
285
292
|
end
|
293
|
+
|
294
|
+
# Extracts id and name from various item formats (objects, hashes, strings)
|
295
|
+
# Returns [id, name] as strings
|
296
|
+
# @param item [Object] The item to extract data from
|
297
|
+
# @param value_method [Symbol] Method to call for the value/id
|
298
|
+
# @param text_method [Symbol] Method to call for display text
|
299
|
+
# NOTE: This method must be public to be accessible from view context
|
300
|
+
def extract_item_data(item, value_method = :id, text_method = :name)
|
301
|
+
# Handle nil items
|
302
|
+
return ['', ''] if item.nil?
|
303
|
+
|
304
|
+
# Handle string items (like tags) - both ID and name are the same
|
305
|
+
return [item, item] if item.is_a?(String)
|
306
|
+
|
307
|
+
# Handle ActiveRecord objects with specified methods
|
308
|
+
if item.respond_to?(value_method) && item.respond_to?(text_method)
|
309
|
+
begin
|
310
|
+
value = item.send(value_method)
|
311
|
+
text = item.send(text_method)
|
312
|
+
return [value.to_s, text.to_s]
|
313
|
+
rescue StandardError
|
314
|
+
# Fallback to default behavior
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# Handle Hash objects with symbol or string keys
|
319
|
+
if item.is_a?(Hash)
|
320
|
+
id = item[value_method] || item[value_method.to_s] || item[:id] || item['id']
|
321
|
+
name = item[text_method] || item[text_method.to_s] || item[:name] || item['name']
|
322
|
+
return [id.to_s, name.to_s]
|
323
|
+
end
|
324
|
+
|
325
|
+
# Handle numeric items
|
326
|
+
return [item.to_s, item.to_s] if item.is_a?(Numeric)
|
327
|
+
|
328
|
+
# Final fallback - treat as string
|
329
|
+
[item.to_s, item.to_s]
|
330
|
+
end
|
286
331
|
end
|
287
332
|
end
|
288
333
|
end
|