rails_notion_like_multiselect 0.2.0 → 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 +44 -0
- data/README.md +21 -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 +111 -88
- 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,50 @@ 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
|
+
|
8
52
|
## [0.2.0] - 2025-09-25
|
9
53
|
|
10
54
|
### Added
|
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
|
|
@@ -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}"
|
@@ -39,69 +39,69 @@ module RailsNotionLikeMultiselect
|
|
39
39
|
# Determine theme-specific classes based on theme option
|
40
40
|
is_dark_theme = theme == 'dark'
|
41
41
|
is_light_theme = theme == 'light'
|
42
|
-
|
42
|
+
|
43
43
|
# Support both light and dark modes with proper contrast
|
44
44
|
badge_classes = if is_dark_theme
|
45
45
|
# Force dark theme styles
|
46
46
|
case badge_color
|
47
47
|
when 'green'
|
48
48
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
49
|
-
|
49
|
+
'bg-green-900/30 text-green-200 border border-green-800'
|
50
50
|
when 'purple'
|
51
51
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
52
|
-
|
52
|
+
'bg-purple-900/30 text-purple-200 border border-purple-800'
|
53
53
|
when 'yellow'
|
54
54
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
55
|
-
|
55
|
+
'bg-yellow-900/30 text-yellow-200 border border-yellow-800'
|
56
56
|
when 'red'
|
57
57
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
58
|
-
|
58
|
+
'bg-red-900/30 text-red-200 border border-red-800'
|
59
59
|
else
|
60
60
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
61
|
-
|
61
|
+
'bg-blue-900/30 text-blue-200 border border-blue-800'
|
62
62
|
end
|
63
63
|
elsif is_light_theme
|
64
64
|
# Force light theme styles
|
65
65
|
case badge_color
|
66
66
|
when 'green'
|
67
67
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
68
|
-
|
68
|
+
'bg-green-100 text-green-800 border border-green-200'
|
69
69
|
when 'purple'
|
70
70
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
71
|
-
|
71
|
+
'bg-purple-100 text-purple-800 border border-purple-200'
|
72
72
|
when 'yellow'
|
73
73
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
74
|
-
|
74
|
+
'bg-yellow-100 text-yellow-800 border border-yellow-200'
|
75
75
|
when 'red'
|
76
76
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
77
|
-
|
77
|
+
'bg-red-100 text-red-800 border border-red-200'
|
78
78
|
else
|
79
79
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
80
|
-
|
80
|
+
'bg-blue-100 text-blue-800 border border-blue-200'
|
81
81
|
end
|
82
82
|
else
|
83
83
|
# Auto mode - use Tailwind's dark: variants
|
84
84
|
case badge_color
|
85
85
|
when 'green'
|
86
86
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
87
|
-
|
88
|
-
|
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'
|
89
89
|
when 'purple'
|
90
90
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
91
|
-
|
92
|
-
|
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'
|
93
93
|
when 'yellow'
|
94
94
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
95
|
-
|
96
|
-
|
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'
|
97
97
|
when 'red'
|
98
98
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
99
|
-
|
100
|
-
|
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'
|
101
101
|
else
|
102
102
|
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium gap-1 ' +
|
103
|
-
|
104
|
-
|
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'
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
@@ -131,26 +131,26 @@ module RailsNotionLikeMultiselect
|
|
131
131
|
|
132
132
|
# Input container with selected items inside
|
133
133
|
input_container_class = if is_dark_theme
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
154
154
|
input_container_html = content_tag :div,
|
155
155
|
class: input_container_class,
|
156
156
|
data: { action: 'click->rails-notion-multiselect#focusInput' } do
|
@@ -161,45 +161,50 @@ module RailsNotionLikeMultiselect
|
|
161
161
|
selected.map do |item|
|
162
162
|
item_id, item_name = extract_item_data(item, value_method, text_method)
|
163
163
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
171
|
+
|
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 +
|
175
178
|
content_tag(:button,
|
176
179
|
type: 'button',
|
177
180
|
data: { action: 'click->rails-notion-multiselect#handleRemove', item_id: item_id },
|
178
181
|
class: button_class) do
|
179
|
-
content_tag(:svg,
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|
188
191
|
tag.path(d: 'M4 4l6 6m0-6l-6 6')
|
189
192
|
end
|
190
193
|
end
|
191
194
|
end
|
195
|
+
|
196
|
+
badge_html
|
192
197
|
end.join.html_safe
|
193
198
|
end
|
194
199
|
|
195
200
|
# Input field
|
196
201
|
input_placeholder_class = if is_dark_theme
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
203
208
|
input_html = tag.input(type: 'text',
|
204
209
|
placeholder: selected.empty? ? placeholder : '',
|
205
210
|
data: { rails_notion_multiselect_target: 'input' },
|
@@ -225,10 +230,10 @@ module RailsNotionLikeMultiselect
|
|
225
230
|
class: 'max-h-60 overflow-auto py-1' do
|
226
231
|
collection.map do |item|
|
227
232
|
item_id, item_name = extract_item_data(item, value_method, text_method)
|
228
|
-
is_selected = selected.any?
|
229
|
-
s_id,
|
233
|
+
is_selected = selected.any? do |s|
|
234
|
+
s_id, = extract_item_data(s, value_method, text_method)
|
230
235
|
s_id == item_id
|
231
|
-
|
236
|
+
end
|
232
237
|
|
233
238
|
content_tag :div,
|
234
239
|
data: {
|
@@ -260,7 +265,7 @@ module RailsNotionLikeMultiselect
|
|
260
265
|
hidden_inputs_html = content_tag :div, data: { rails_notion_multiselect_target: 'hiddenInputs' } do
|
261
266
|
if selected.any?
|
262
267
|
selected.map do |item|
|
263
|
-
item_id,
|
268
|
+
item_id, = extract_item_data(item, value_method, text_method)
|
264
269
|
tag.input(type: 'hidden', name: input_name, value: item_id)
|
265
270
|
end.join.html_safe
|
266
271
|
else
|
@@ -284,26 +289,44 @@ module RailsNotionLikeMultiselect
|
|
284
289
|
|
285
290
|
label_html + input_container_html + dropdown_html + hidden_inputs_html + help_text_html
|
286
291
|
end
|
292
|
+
end
|
287
293
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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
|
305
315
|
end
|
306
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]
|
307
330
|
end
|
308
331
|
end
|
309
332
|
end
|