maquina-components 0.4.2 → 0.4.3

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: 55a2ec2879afb8976c7b668873da307a50d2a2798e003399dbc8c2d4d9779471
4
- data.tar.gz: 2db272ca8f735467c06d2f37b133fb7da4156d9662b85cb19ec93ac9d84078f7
3
+ metadata.gz: b709941be68c552761043a38b7359796b7bc644af13827fbd47b2f40eab90fb9
4
+ data.tar.gz: 931d38949a68e47881e9dc417786a412e5fb16ed02a23539d44259f452249913
5
5
  SHA512:
6
- metadata.gz: 719870f407619f1f4fe5714db8e77af06db45ae3272a58784f647c4280b8ef78d0c1e805e95b58ccdd2f439392652f1483f054c421b093621472bac8cfc34de9
7
- data.tar.gz: 4bc447930b1e71135ef1012455d82a78de7be2483ba17fae25185239a459185b0ed29278ac49bc40c086ade7a8ed2bbc0bd248f4cce8505a26897c273e96246f
6
+ metadata.gz: 3b063e95e3ea3c7d4fd649b75cd00696ac33ac88025c3ebab14bea256f7456005a81b8e0c7f3b70f8904f73cc987c4d5cc22a707cbef48f3d10dafa21a7d9ec1
7
+ data.tar.gz: 80b31cd01a92351baedec4a87509b494c6dbfe09cb42bb71c7a59444cca0cc48e4b020684d9c450c1b02568a352f0b2781654c5333a84a0c5af7ea38940f91a6
@@ -107,6 +107,29 @@
107
107
  }
108
108
 
109
109
  /* ===== Responsive Behavior ===== */
110
+ /* Force nowrap when responsive controller is active so scrollWidth detects overflow */
111
+ [data-controller="breadcrumb"] [data-breadcrumb-part="list"] {
112
+ @apply flex-nowrap overflow-hidden;
113
+ }
114
+
115
+ /* Items must not shrink or wrap text, otherwise flex compresses them and scrollWidth never exceeds clientWidth */
116
+ [data-controller="breadcrumb"] [data-breadcrumb-part="item"],
117
+ [data-controller="breadcrumb"] [data-breadcrumb-part="separator"] {
118
+ @apply shrink-0 whitespace-nowrap;
119
+ }
120
+
121
+ /* Allow the last item (current page) to truncate gracefully instead of hard-clipping */
122
+ [data-controller="breadcrumb"] [data-breadcrumb-part="item"]:last-child {
123
+ @apply shrink min-w-0 overflow-hidden;
124
+ text-overflow: ellipsis;
125
+ }
126
+
127
+ [data-controller="breadcrumb"] [data-breadcrumb-part="item"]:last-child [data-breadcrumb-part="page"],
128
+ [data-controller="breadcrumb"] [data-breadcrumb-part="item"]:last-child [data-breadcrumb-part="link"] {
129
+ @apply overflow-hidden;
130
+ text-overflow: ellipsis;
131
+ }
132
+
110
133
  /* Items hidden by Stimulus controller */
111
134
  [data-controller="breadcrumb"] [data-breadcrumb-target="item"].hidden,
112
135
  [data-controller="breadcrumb"] [data-breadcrumb-target="ellipsisSeparator"].hidden {
@@ -4,6 +4,13 @@ export default class extends Controller {
4
4
  static targets = ["item", "ellipsis", "ellipsisSeparator"]
5
5
 
6
6
  connect() {
7
+ this._dropdown = null
8
+ this._clickOutsideHandler = this._closeDropdown.bind(this)
9
+ this._escapeHandler = this._handleEscape.bind(this)
10
+ this._teardownHandler = this._teardown.bind(this)
11
+
12
+ document.addEventListener("turbo:before-cache", this._teardownHandler)
13
+
7
14
  this.windowResizeHandler = this.handleResize.bind(this)
8
15
  window.addEventListener('resize', this.windowResizeHandler)
9
16
  this.handleResize()
@@ -11,61 +18,185 @@ export default class extends Controller {
11
18
 
12
19
  disconnect() {
13
20
  window.removeEventListener('resize', this.windowResizeHandler)
21
+ this._teardown()
22
+ document.removeEventListener("turbo:before-cache", this._teardownHandler)
23
+ }
24
+
25
+ ellipsisTargetConnected(element) {
26
+ const trigger = element.querySelector('[data-breadcrumb-part="ellipsis"]')
27
+ if (trigger) {
28
+ this._ellipsisTrigger = trigger
29
+ this._toggleHandler = this._toggleDropdown.bind(this)
30
+ trigger.addEventListener('click', this._toggleHandler)
31
+ trigger.style.cursor = 'pointer'
32
+ }
33
+ }
34
+
35
+ ellipsisTargetDisconnected(_element) {
36
+ if (this._ellipsisTrigger && this._toggleHandler) {
37
+ this._ellipsisTrigger.removeEventListener('click', this._toggleHandler)
38
+ this._ellipsisTrigger = null
39
+ this._toggleHandler = null
40
+ }
14
41
  }
15
42
 
16
43
  handleResize() {
17
- // Get visible width of container
18
- const containerWidth = this.element.clientWidth
44
+ const list = this.element.querySelector('[data-breadcrumb-part="list"]')
45
+ if (!list) return
46
+
19
47
  const items = this.itemTargets
20
48
  const ellipsis = this.hasEllipsisTarget ? this.ellipsisTarget : null
21
49
  const ellipsisSeparator = this.hasEllipsisSeparatorTarget ? this.ellipsisSeparatorTarget : null
22
50
 
23
- // Always show first and last items
24
- if (items.length < 3 || !ellipsis) {
25
- return; // Not enough items to collapse or no ellipsis element
26
- }
51
+ if (items.length < 1 || !ellipsis) return
27
52
 
28
- // Reset visibility
29
- if (ellipsis) ellipsis.classList.add('hidden')
53
+ // Reset all items and their adjacent separators to visible
54
+ ellipsis.classList.add('hidden')
30
55
  if (ellipsisSeparator) ellipsisSeparator.classList.add('hidden')
31
-
32
56
  items.forEach(item => {
33
57
  item.classList.remove('hidden')
58
+ const sep = this._adjacentSeparator(item)
59
+ if (sep) sep.classList.remove('hidden')
34
60
  })
35
61
 
36
- // Check if we need to collapse items
37
- let totalWidth = 0
38
- items.forEach(item => {
39
- totalWidth += item.offsetWidth
40
- })
41
-
42
- if (totalWidth > containerWidth) {
43
- // We need to collapse items - show ellipsis
44
- if (ellipsis) ellipsis.classList.remove('hidden')
62
+ // Check overflow using scrollWidth vs clientWidth
63
+ if (list.scrollWidth > list.clientWidth) {
64
+ ellipsis.classList.remove('hidden')
45
65
  if (ellipsisSeparator) ellipsisSeparator.classList.remove('hidden')
46
66
 
47
- // Start hiding middle items until we fit
48
- for (let i = items.length - 2; i > 0; i--) {
49
- if (i !== 0 && i !== items.length - 1) {
50
- items[i].classList.add('hidden')
67
+ // Hide middle items one at a time until it fits
68
+ for (let i = items.length - 1; i >= 0; i--) {
69
+ items[i].classList.add('hidden')
70
+ const sep = this._adjacentSeparator(items[i])
71
+ if (sep) sep.classList.add('hidden')
72
+ if (list.scrollWidth <= list.clientWidth) break
73
+ }
74
+ }
51
75
 
52
- // Recalculate total width
53
- totalWidth = 0
76
+ this._updateDropdown()
77
+ }
54
78
 
55
- if (ellipsis) totalWidth += ellipsis.offsetWidth
56
- if (ellipsisSeparator) totalWidth += ellipsisSeparator.offsetWidth
79
+ // Find the next sibling separator <li> (not the managed ellipsisSeparator)
80
+ _adjacentSeparator(item) {
81
+ const next = item.nextElementSibling
82
+ if (next && next.dataset.breadcrumbPart === "separator" && !next.dataset.breadcrumbTarget) {
83
+ return next
84
+ }
85
+ return null
86
+ }
87
+
88
+ // Collect hidden items and update dropdown content
89
+ _updateDropdown() {
90
+ const hiddenItems = this.itemTargets.filter(item => item.classList.contains('hidden'))
57
91
 
58
- items.forEach(item => {
59
- if (!item.classList.contains('hidden')) {
60
- totalWidth += item.offsetWidth
61
- }
62
- })
92
+ if (hiddenItems.length === 0) {
93
+ this._removeDropdown()
94
+ return
95
+ }
63
96
 
64
- if (totalWidth <= containerWidth) {
65
- break
66
- }
67
- }
97
+ // Build list of links from hidden items
98
+ this._hiddenLinks = hiddenItems.map(item => {
99
+ const link = item.querySelector('[data-breadcrumb-part="link"]')
100
+ if (link) {
101
+ return { href: link.getAttribute('href'), text: link.textContent.trim() }
68
102
  }
103
+ return null
104
+ }).filter(Boolean)
105
+
106
+ // If dropdown is currently open, rebuild its content
107
+ if (this._dropdown && this._dropdown.dataset.state === "open") {
108
+ this._buildDropdownContent()
69
109
  }
70
110
  }
111
+
112
+ _toggleDropdown(event) {
113
+ event.stopPropagation()
114
+
115
+ if (this._dropdown && this._dropdown.dataset.state === "open") {
116
+ this._closeDropdown()
117
+ } else {
118
+ this._openDropdown()
119
+ }
120
+ }
121
+
122
+ _openDropdown() {
123
+ if (!this._hiddenLinks || this._hiddenLinks.length === 0) return
124
+
125
+ if (!this._dropdown) {
126
+ this._dropdown = document.createElement('div')
127
+ this._dropdown.setAttribute('role', 'menu')
128
+ this._dropdown.dataset.dropdownMenuPart = 'content'
129
+ this._dropdown.style.position = 'fixed'
130
+ this._dropdown.style.zIndex = '50'
131
+ document.body.appendChild(this._dropdown)
132
+ }
133
+
134
+ this._buildDropdownContent()
135
+ this._positionDropdown()
136
+ this._dropdown.dataset.state = 'open'
137
+
138
+ if (this._ellipsisTrigger) {
139
+ this._ellipsisTrigger.dataset.state = 'open'
140
+ }
141
+
142
+ // Defer listeners so the current click doesn't immediately close
143
+ requestAnimationFrame(() => {
144
+ document.addEventListener('click', this._clickOutsideHandler)
145
+ document.addEventListener('keydown', this._escapeHandler)
146
+ })
147
+ }
148
+
149
+ _closeDropdown() {
150
+ if (this._dropdown) {
151
+ this._dropdown.dataset.state = 'closed'
152
+ }
153
+ if (this._ellipsisTrigger) {
154
+ delete this._ellipsisTrigger.dataset.state
155
+ }
156
+
157
+ document.removeEventListener('click', this._clickOutsideHandler)
158
+ document.removeEventListener('keydown', this._escapeHandler)
159
+ }
160
+
161
+ _removeDropdown() {
162
+ this._closeDropdown()
163
+ if (this._dropdown) {
164
+ this._dropdown.remove()
165
+ this._dropdown = null
166
+ }
167
+ }
168
+
169
+ _buildDropdownContent() {
170
+ if (!this._dropdown || !this._hiddenLinks) return
171
+
172
+ this._dropdown.innerHTML = ''
173
+ this._hiddenLinks.forEach(({ href, text }) => {
174
+ const link = document.createElement('a')
175
+ link.setAttribute('href', href)
176
+ link.setAttribute('role', 'menuitem')
177
+ link.dataset.dropdownMenuPart = 'item'
178
+ link.setAttribute('tabindex', '-1')
179
+ link.textContent = text
180
+ this._dropdown.appendChild(link)
181
+ })
182
+ }
183
+
184
+ _positionDropdown() {
185
+ if (!this._dropdown || !this._ellipsisTrigger) return
186
+
187
+ const rect = this._ellipsisTrigger.getBoundingClientRect()
188
+ this._dropdown.style.top = `${rect.bottom + 4}px`
189
+ this._dropdown.style.left = `${rect.left}px`
190
+ }
191
+
192
+ _handleEscape(event) {
193
+ if (event.key === 'Escape') {
194
+ this._closeDropdown()
195
+ }
196
+ }
197
+
198
+ _teardown() {
199
+ this._removeDropdown()
200
+ this._hiddenLinks = null
201
+ }
71
202
  }
@@ -1,3 +1,3 @@
1
1
  module MaquinaComponents
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maquina-components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Alberto Chávez