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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e99bd8d6c2dd51ca270fa203e6f7ef9dbf7f5dee8e651de45127e548f9e18ef7
4
- data.tar.gz: 6cb8d559d809f3ee124eb28dd5511f57ba98d4452fe74b720198029ec5ffc61e
3
+ metadata.gz: 0053eb09bf9bc9d733102ffc6c24d72ee8de94dcc968a8003b186621a8c57d7e
4
+ data.tar.gz: a7d3554230efc1f34df5c2b29517dff2434b38b4608a9699b2382a3b92150328
5
5
  SHA512:
6
- metadata.gz: ad3946fdb3dd98c00be6a078dfede6d8f3d68526929dfd3baab054f99fa98a60617b26486ca9587f6ab16fd06832222e9f69f67a7d8c60139956cc7a1e9e77b7
7
- data.tar.gz: 704516ab3dcb262260573e4870f85520ff361b4b374a96548c8e46121f77fd9407b4b9af2571f3dc3c36d1873baa22be013d4bc1dafaff96df1dd009f580d6d8
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.1.1'
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 your application
79
- - Register the controller in your Stimulus application
80
- - Create an initializer with default configuration
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 hidden inputs to restore selections
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
- existingInputs.forEach((input, index) => {
71
- const itemId = String(input.value) // Ensure ID is a string
72
- if (itemId) {
73
- const badge = existingBadges[index]
74
- if (badge) {
75
- const nameText = badge.querySelector('[data-item-name]')?.textContent?.trim() ||
76
- badge.childNodes[0]?.textContent?.trim() || ''
77
- if (nameText) {
78
- this.selectedItems.set(itemId, nameText)
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 hidden inputs to restore selections
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
- existingInputs.forEach((input, index) => {
71
- const itemId = String(input.value) // Ensure ID is a string
72
- if (itemId) {
73
- const badge = existingBadges[index]
74
- if (badge) {
75
- const nameText = badge.querySelector('[data-item-name]')?.textContent?.trim() ||
76
- badge.childNodes[0]?.textContent?.trim() || ''
77
- if (nameText) {
78
- this.selectedItems.set(itemId, nameText)
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
- 'bg-green-900/30 text-green-200 border border-green-800'
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
- 'bg-purple-900/30 text-purple-200 border border-purple-800'
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
- 'bg-yellow-900/30 text-yellow-200 border border-yellow-800'
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
- 'bg-red-900/30 text-red-200 border border-red-800'
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
- 'bg-blue-900/30 text-blue-200 border border-blue-800'
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
- 'bg-green-100 text-green-800 border border-green-200'
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
- 'bg-purple-100 text-purple-800 border border-purple-200'
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
- 'bg-yellow-100 text-yellow-800 border border-yellow-200'
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
- 'bg-red-100 text-red-800 border border-red-200'
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
- 'bg-blue-100 text-blue-800 border border-blue-200'
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
- 'bg-green-100 text-green-800 border border-green-200 ' +
88
- 'dark:bg-green-900/30 dark:text-green-200 dark:border-green-800'
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
- 'bg-purple-100 text-purple-800 border border-purple-200 ' +
92
- 'dark:bg-purple-900/30 dark:text-purple-200 dark:border-purple-800'
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
- 'bg-yellow-100 text-yellow-800 border border-yellow-200 ' +
96
- 'dark:bg-yellow-900/30 dark:text-yellow-200 dark:border-yellow-800'
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
- 'bg-red-100 text-red-800 border border-red-200 ' +
100
- 'dark:bg-red-900/30 dark:text-red-200 dark:border-red-800'
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
- 'bg-blue-100 text-blue-800 border border-blue-200 ' +
104
- 'dark:bg-blue-900/30 dark:text-blue-200 dark:border-blue-800'
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
- '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
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
- content_tag :span,
165
- class: badge_classes,
166
- data: { item_id: item_id } do
167
- content_tag(:span, item_name, data: { item_name: true }) +
168
- button_class = if is_dark_theme
169
- 'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-400/20'
170
- elsif is_light_theme
171
- 'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20'
172
- else
173
- 'ml-1 group relative h-3.5 w-3.5 rounded-sm hover:bg-gray-600/20 dark:hover:bg-gray-400/20'
174
- end
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
- xmlns: 'http://www.w3.org/2000/svg',
181
- viewBox: '0 0 14 14',
182
- fill: 'none',
183
- stroke: 'currentColor',
184
- 'stroke-width': '2',
185
- 'stroke-linecap': 'round',
186
- 'stroke-linejoin': 'round',
187
- class: 'h-3.5 w-3.5 opacity-60 group-hover:opacity-100') do
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
- 'placeholder-gray-500'
198
- elsif is_light_theme
199
- 'placeholder-gray-400'
200
- else
201
- 'placeholder-gray-400 dark:placeholder-gray-500'
202
- end
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? { |s|
229
- s_id, _ = extract_item_data(s, value_method, text_method)
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, _ = extract_item_data(item, value_method, text_method)
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
- # Extracts id and name from various item formats (objects, hashes, strings)
289
- # Returns [id, name] as strings
290
- # @param item [Object] The item to extract data from
291
- # @param value_method [Symbol] Method to call for the value/id
292
- # @param text_method [Symbol] Method to call for display text
293
- def extract_item_data(item, value_method = :id, text_method = :name)
294
- if item.respond_to?(value_method) && item.respond_to?(text_method)
295
- # ActiveRecord object or similar with custom methods
296
- [item.send(value_method).to_s, item.send(text_method).to_s]
297
- elsif item.is_a?(Hash)
298
- # Hash with symbol or string keys
299
- id = item[value_method] || item[value_method.to_s] || item[:id] || item['id']
300
- name = item[text_method] || item[text_method.to_s] || item[:name] || item['name']
301
- [id.to_s, name.to_s]
302
- else
303
- # Fallback - treat as string/number (both value and display are the same)
304
- [item.to_s, item.to_s]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsNotionLikeMultiselect
4
- VERSION = "0.2.0"
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_notion_like_multiselect
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sulman Baig