rip-lang 3.13.93 → 3.13.95
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.
- package/README.md +1 -1
- package/docs/dist/rip.js +142 -38
- package/docs/dist/rip.min.js +174 -174
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +1 -1
- package/src/ui.rip +65 -0
- package/docs/ui/accordion.rip +0 -113
- package/docs/ui/alert-dialog.rip +0 -96
- package/docs/ui/autocomplete.rip +0 -141
- package/docs/ui/avatar.rip +0 -37
- package/docs/ui/badge.rip +0 -15
- package/docs/ui/breadcrumb.rip +0 -46
- package/docs/ui/button-group.rip +0 -26
- package/docs/ui/button.rip +0 -23
- package/docs/ui/card.rip +0 -25
- package/docs/ui/carousel.rip +0 -110
- package/docs/ui/checkbox-group.rip +0 -65
- package/docs/ui/checkbox.rip +0 -33
- package/docs/ui/collapsible.rip +0 -50
- package/docs/ui/combobox.rip +0 -155
- package/docs/ui/context-menu.rip +0 -105
- package/docs/ui/date-picker.rip +0 -214
- package/docs/ui/dialog.rip +0 -107
- package/docs/ui/drawer.rip +0 -79
- package/docs/ui/editable-value.rip +0 -80
- package/docs/ui/field.rip +0 -53
- package/docs/ui/fieldset.rip +0 -22
- package/docs/ui/form.rip +0 -39
- package/docs/ui/grid.rip +0 -901
- package/docs/ui/hljs-rip.js +0 -209
- package/docs/ui/index.css +0 -1772
- package/docs/ui/index.html +0 -2433
- package/docs/ui/input-group.rip +0 -28
- package/docs/ui/input.rip +0 -36
- package/docs/ui/label.rip +0 -16
- package/docs/ui/menu.rip +0 -162
- package/docs/ui/menubar.rip +0 -155
- package/docs/ui/meter.rip +0 -36
- package/docs/ui/multi-select.rip +0 -158
- package/docs/ui/native-select.rip +0 -32
- package/docs/ui/nav-menu.rip +0 -129
- package/docs/ui/number-field.rip +0 -162
- package/docs/ui/otp-field.rip +0 -89
- package/docs/ui/pagination.rip +0 -123
- package/docs/ui/popover.rip +0 -143
- package/docs/ui/preview-card.rip +0 -73
- package/docs/ui/progress.rip +0 -25
- package/docs/ui/radio-group.rip +0 -67
- package/docs/ui/resizable.rip +0 -123
- package/docs/ui/scroll-area.rip +0 -145
- package/docs/ui/select.rip +0 -184
- package/docs/ui/separator.rip +0 -17
- package/docs/ui/skeleton.rip +0 -22
- package/docs/ui/slider.rip +0 -165
- package/docs/ui/spinner.rip +0 -17
- package/docs/ui/table.rip +0 -27
- package/docs/ui/tabs.rip +0 -124
- package/docs/ui/textarea.rip +0 -48
- package/docs/ui/toast.rip +0 -87
- package/docs/ui/toggle-group.rip +0 -78
- package/docs/ui/toggle.rip +0 -24
- package/docs/ui/toolbar.rip +0 -46
- package/docs/ui/tooltip.rip +0 -115
package/docs/ui/carousel.rip
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# Carousel — accessible headless slide carousel
|
|
2
|
-
#
|
|
3
|
-
# Displays one slide at a time with arrow key navigation, optional
|
|
4
|
-
# autoplay, and loop mode. Discovers slides from [data-slide] children.
|
|
5
|
-
# Ships zero CSS.
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# Carousel loop: true
|
|
9
|
-
# div $slide: true
|
|
10
|
-
# img src: "slide1.jpg"
|
|
11
|
-
# div $slide: true
|
|
12
|
-
# img src: "slide2.jpg"
|
|
13
|
-
# div $slide: true
|
|
14
|
-
# img src: "slide3.jpg"
|
|
15
|
-
#
|
|
16
|
-
# Carousel autoplay: true, interval: 5000, @change: handleSlide
|
|
17
|
-
# div $slide: true, "Slide A"
|
|
18
|
-
# div $slide: true, "Slide B"
|
|
19
|
-
|
|
20
|
-
export Carousel = component
|
|
21
|
-
@orientation := 'horizontal'
|
|
22
|
-
@loop := false
|
|
23
|
-
@autoplay := false
|
|
24
|
-
@interval := 4000
|
|
25
|
-
@label := 'Carousel'
|
|
26
|
-
|
|
27
|
-
activeIndex := 0
|
|
28
|
-
_ready := false
|
|
29
|
-
_timer = null
|
|
30
|
-
|
|
31
|
-
_slides ~=
|
|
32
|
-
return [] unless _ready
|
|
33
|
-
return [] unless @_content
|
|
34
|
-
Array.from(@_content.querySelectorAll('[data-slide]') or [])
|
|
35
|
-
|
|
36
|
-
totalSlides ~= _slides.length
|
|
37
|
-
|
|
38
|
-
mounted: ->
|
|
39
|
-
_ready = true
|
|
40
|
-
@_startAutoplay() if @autoplay
|
|
41
|
-
|
|
42
|
-
beforeUnmount: ->
|
|
43
|
-
@_stopAutoplay()
|
|
44
|
-
|
|
45
|
-
_startAutoplay: ->
|
|
46
|
-
@_stopAutoplay()
|
|
47
|
-
_timer = setInterval (=> @next()), @interval
|
|
48
|
-
|
|
49
|
-
_stopAutoplay: ->
|
|
50
|
-
clearInterval _timer if _timer
|
|
51
|
-
_timer = null
|
|
52
|
-
|
|
53
|
-
goto: (idx) ->
|
|
54
|
-
count = totalSlides
|
|
55
|
-
return unless count
|
|
56
|
-
if @loop
|
|
57
|
-
idx = idx %% count
|
|
58
|
-
else
|
|
59
|
-
idx = Math.max(0, Math.min(idx, count - 1))
|
|
60
|
-
activeIndex = idx
|
|
61
|
-
@emit 'change', activeIndex
|
|
62
|
-
|
|
63
|
-
next: -> @goto(activeIndex + 1)
|
|
64
|
-
prev: -> @goto(activeIndex - 1)
|
|
65
|
-
|
|
66
|
-
onKeydown: (e) ->
|
|
67
|
-
horiz = @orientation is 'horizontal'
|
|
68
|
-
switch e.key
|
|
69
|
-
when (if horiz then 'ArrowRight' else 'ArrowDown')
|
|
70
|
-
e.preventDefault()
|
|
71
|
-
@next()
|
|
72
|
-
when (if horiz then 'ArrowLeft' else 'ArrowUp')
|
|
73
|
-
e.preventDefault()
|
|
74
|
-
@prev()
|
|
75
|
-
when 'Home'
|
|
76
|
-
e.preventDefault()
|
|
77
|
-
@goto(0)
|
|
78
|
-
when 'End'
|
|
79
|
-
e.preventDefault()
|
|
80
|
-
@goto(totalSlides - 1)
|
|
81
|
-
|
|
82
|
-
~>
|
|
83
|
-
return unless _ready
|
|
84
|
-
_slides.forEach (el, idx) =>
|
|
85
|
-
isActive = idx is activeIndex
|
|
86
|
-
el.hidden = not isActive
|
|
87
|
-
el.toggleAttribute 'data-active', isActive
|
|
88
|
-
el.setAttribute 'role', 'tabpanel'
|
|
89
|
-
el.setAttribute 'aria-roledescription', 'slide'
|
|
90
|
-
el.setAttribute 'aria-label', "Slide #{idx + 1} of #{totalSlides}"
|
|
91
|
-
|
|
92
|
-
onMouseenter: -> @_stopAutoplay() if @autoplay
|
|
93
|
-
onMouseleave: -> @_startAutoplay() if @autoplay
|
|
94
|
-
|
|
95
|
-
render
|
|
96
|
-
div role: "region", aria-roledescription: "carousel", aria-label: @label, tabindex: "0"
|
|
97
|
-
$orientation: @orientation
|
|
98
|
-
@keydown: @onKeydown
|
|
99
|
-
@mouseenter: @onMouseenter
|
|
100
|
-
@mouseleave: @onMouseleave
|
|
101
|
-
button $prev: true, aria-label: "Previous slide"
|
|
102
|
-
disabled: not @loop and activeIndex <= 0
|
|
103
|
-
$disabled: (not @loop and activeIndex <= 0)?!
|
|
104
|
-
@click: (=> @prev())
|
|
105
|
-
div ref: "_content"
|
|
106
|
-
slot
|
|
107
|
-
button $next: true, aria-label: "Next slide"
|
|
108
|
-
disabled: not @loop and activeIndex >= totalSlides - 1
|
|
109
|
-
$disabled: (not @loop and activeIndex >= totalSlides - 1)?!
|
|
110
|
-
@click: (=> @next())
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# CheckboxGroup — accessible headless checkbox group
|
|
2
|
-
#
|
|
3
|
-
# Multiple options can be checked independently. Wraps individual checkboxes
|
|
4
|
-
# with group semantics. Value is an array of checked option values.
|
|
5
|
-
# Ships zero CSS.
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# CheckboxGroup value <=> selectedToppings
|
|
9
|
-
# div $value: "cheese", "Cheese"
|
|
10
|
-
# div $value: "bacon", "Bacon"
|
|
11
|
-
# div $value: "lettuce", "Lettuce"
|
|
12
|
-
|
|
13
|
-
export CheckboxGroup = component
|
|
14
|
-
@value := []
|
|
15
|
-
@disabled := false
|
|
16
|
-
@orientation := 'vertical'
|
|
17
|
-
@label := ''
|
|
18
|
-
|
|
19
|
-
_options ~=
|
|
20
|
-
return [] unless @_slot
|
|
21
|
-
Array.from(@_slot.querySelectorAll('[data-value]') or [])
|
|
22
|
-
|
|
23
|
-
_isChecked: (val) ->
|
|
24
|
-
Array.isArray(@value) and val in @value
|
|
25
|
-
|
|
26
|
-
_toggle: (val) ->
|
|
27
|
-
return if @disabled
|
|
28
|
-
arr = if Array.isArray(@value) then [...@value] else []
|
|
29
|
-
if val in arr
|
|
30
|
-
arr = arr.filter (v) -> v isnt val
|
|
31
|
-
else
|
|
32
|
-
arr.push val
|
|
33
|
-
@value = arr
|
|
34
|
-
@emit 'change', @value
|
|
35
|
-
|
|
36
|
-
onKeydown: (e) ->
|
|
37
|
-
boxes = @_root?.querySelectorAll('[role="checkbox"]')
|
|
38
|
-
return unless boxes?.length
|
|
39
|
-
focused = Array.from(boxes).indexOf(document.activeElement)
|
|
40
|
-
return if focused < 0
|
|
41
|
-
len = boxes.length
|
|
42
|
-
switch e.key
|
|
43
|
-
when 'ArrowDown', 'ArrowRight'
|
|
44
|
-
e.preventDefault()
|
|
45
|
-
boxes[(focused + 1) %% len]?.focus()
|
|
46
|
-
when 'ArrowUp', 'ArrowLeft'
|
|
47
|
-
e.preventDefault()
|
|
48
|
-
boxes[(focused - 1) %% len]?.focus()
|
|
49
|
-
|
|
50
|
-
render
|
|
51
|
-
div ref: "_root", role: "group", aria-label: @label or undefined, aria-orientation: @orientation
|
|
52
|
-
$orientation: @orientation
|
|
53
|
-
$disabled: @disabled?!
|
|
54
|
-
|
|
55
|
-
. ref: "_slot", style: "display:none"
|
|
56
|
-
slot
|
|
57
|
-
|
|
58
|
-
for opt, idx in _options
|
|
59
|
-
button role: "checkbox", tabindex: (if idx is 0 then "0" else "-1")
|
|
60
|
-
aria-checked: !!@_isChecked(opt.dataset.value)
|
|
61
|
-
$checked: @_isChecked(opt.dataset.value)?!
|
|
62
|
-
$disabled: @disabled?!
|
|
63
|
-
$value: opt.dataset.value
|
|
64
|
-
@click: (=> @_toggle(opt.dataset.value))
|
|
65
|
-
= opt.textContent
|
package/docs/ui/checkbox.rip
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Checkbox — accessible headless checkbox/switch widget
|
|
2
|
-
#
|
|
3
|
-
# Toggles on click, Enter, or Space. Supports indeterminate state.
|
|
4
|
-
# Exposes $checked, $indeterminate, $disabled. Ships zero CSS.
|
|
5
|
-
# Set @switch to true for switch semantics (role="switch").
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# Checkbox checked <=> isActive, @change: handleChange
|
|
9
|
-
# span "Enable notifications"
|
|
10
|
-
#
|
|
11
|
-
# Checkbox checked <=> isDark, switch: true
|
|
12
|
-
# span "Dark mode"
|
|
13
|
-
|
|
14
|
-
export Checkbox = component
|
|
15
|
-
@checked := false
|
|
16
|
-
@disabled := false
|
|
17
|
-
@indeterminate := false
|
|
18
|
-
@switch := false
|
|
19
|
-
|
|
20
|
-
onClick: ->
|
|
21
|
-
return if @disabled
|
|
22
|
-
@indeterminate = false
|
|
23
|
-
@checked = not @checked
|
|
24
|
-
@emit 'change', @checked
|
|
25
|
-
|
|
26
|
-
render
|
|
27
|
-
button role: @switch ? 'switch' : 'checkbox'
|
|
28
|
-
aria-checked: @indeterminate ? 'mixed' : !!@checked
|
|
29
|
-
aria-disabled: @disabled?!
|
|
30
|
-
$checked: @checked?!
|
|
31
|
-
$indeterminate: @indeterminate?!
|
|
32
|
-
$disabled: @disabled?!
|
|
33
|
-
slot
|
package/docs/ui/collapsible.rip
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Collapsible — accessible headless expand/collapse section
|
|
2
|
-
#
|
|
3
|
-
# Single open/close section. Simpler than Accordion (no item IDs,
|
|
4
|
-
# no single/multiple mode). Exposes content dimensions as CSS
|
|
5
|
-
# custom properties for animated expand/collapse. Ships zero CSS.
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# Collapsible open <=> isOpen
|
|
9
|
-
# button $trigger: true, "Show details"
|
|
10
|
-
# div $content: true
|
|
11
|
-
# p "Hidden content here"
|
|
12
|
-
|
|
13
|
-
export Collapsible = component
|
|
14
|
-
@open := false
|
|
15
|
-
@disabled := false
|
|
16
|
-
|
|
17
|
-
_ready := false
|
|
18
|
-
|
|
19
|
-
mounted: ->
|
|
20
|
-
_ready = true
|
|
21
|
-
trigger = @_root?.querySelector('[data-trigger]')
|
|
22
|
-
return unless trigger
|
|
23
|
-
trigger.addEventListener 'click', => @toggle() unless @disabled
|
|
24
|
-
trigger.addEventListener 'keydown', (e) =>
|
|
25
|
-
if e.key in ['Enter', ' '] and not @disabled
|
|
26
|
-
e.preventDefault()
|
|
27
|
-
@toggle()
|
|
28
|
-
|
|
29
|
-
toggle: ->
|
|
30
|
-
@open = not @open
|
|
31
|
-
@emit 'change', @open
|
|
32
|
-
|
|
33
|
-
~>
|
|
34
|
-
return unless _ready
|
|
35
|
-
trigger = @_root?.querySelector('[data-trigger]')
|
|
36
|
-
content = @_root?.querySelector('[data-content]')
|
|
37
|
-
if trigger
|
|
38
|
-
trigger.setAttribute 'aria-expanded', !!@open
|
|
39
|
-
trigger.setAttribute 'aria-disabled', true if @disabled
|
|
40
|
-
trigger.tabIndex = if @disabled then -1 else 0
|
|
41
|
-
if content
|
|
42
|
-
content.hidden = not @open
|
|
43
|
-
if @open
|
|
44
|
-
rect = content.getBoundingClientRect()
|
|
45
|
-
content.style.setProperty '--collapsible-height', "#{rect.height}px"
|
|
46
|
-
content.style.setProperty '--collapsible-width', "#{rect.width}px"
|
|
47
|
-
|
|
48
|
-
render
|
|
49
|
-
div ref: "_root", $open: @open?!, $disabled: @disabled?!
|
|
50
|
-
slot
|
package/docs/ui/combobox.rip
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
# Combobox — accessible headless combobox (input + filterable listbox)
|
|
2
|
-
#
|
|
3
|
-
# Keyboard: ArrowDown/Up to navigate, Enter to select, Escape to close,
|
|
4
|
-
# typing filters the list via the @filter callback.
|
|
5
|
-
#
|
|
6
|
-
# Exposes $open on the wrapper, $highlighted on options.
|
|
7
|
-
# Ships zero CSS — style entirely via attribute selectors in your stylesheet.
|
|
8
|
-
#
|
|
9
|
-
# Usage:
|
|
10
|
-
# Combobox query <=> searchText, items: fruits, @select: handleSelect, @filter: filterFn
|
|
11
|
-
|
|
12
|
-
export Combobox = component
|
|
13
|
-
@query := ''
|
|
14
|
-
@items := []
|
|
15
|
-
@placeholder := 'Search...'
|
|
16
|
-
@disabled := false
|
|
17
|
-
@autoHighlight := true
|
|
18
|
-
|
|
19
|
-
open := false
|
|
20
|
-
highlightedIndex := -1
|
|
21
|
-
_listId =! "cb-#{Math.random().toString(36).slice(2, 8)}"
|
|
22
|
-
|
|
23
|
-
getItems: ->
|
|
24
|
-
return [] unless @_list
|
|
25
|
-
Array.from(@_list.querySelectorAll('[role="option"]'))
|
|
26
|
-
|
|
27
|
-
_scrollToItem: ->
|
|
28
|
-
@getItems()[highlightedIndex]?.scrollIntoView({ block: 'nearest' })
|
|
29
|
-
|
|
30
|
-
clear: ->
|
|
31
|
-
@query = ''
|
|
32
|
-
highlightedIndex = -1
|
|
33
|
-
@emit 'filter', ''
|
|
34
|
-
|
|
35
|
-
onInput: (e) ->
|
|
36
|
-
@query = e.target.value
|
|
37
|
-
open = true
|
|
38
|
-
highlightedIndex = if @autoHighlight and @items.length > 0 then 0 else -1
|
|
39
|
-
@emit 'filter', @query
|
|
40
|
-
|
|
41
|
-
onFocusin: -> @openMenu()
|
|
42
|
-
|
|
43
|
-
onFocusout: (e) ->
|
|
44
|
-
unless @_content?.contains(e.relatedTarget)
|
|
45
|
-
@close()
|
|
46
|
-
|
|
47
|
-
openMenu: ->
|
|
48
|
-
open = true
|
|
49
|
-
highlightedIndex = -1
|
|
50
|
-
setTimeout => @_position(), 0
|
|
51
|
-
|
|
52
|
-
close: ->
|
|
53
|
-
open = false
|
|
54
|
-
highlightedIndex = -1
|
|
55
|
-
|
|
56
|
-
_position: ->
|
|
57
|
-
return unless @_input and @_list
|
|
58
|
-
tr = @_input.getBoundingClientRect()
|
|
59
|
-
@_list.style.left = "#{tr.left}px"
|
|
60
|
-
@_list.style.top = "#{tr.bottom + 2}px"
|
|
61
|
-
@_list.style.minWidth = "#{tr.width}px"
|
|
62
|
-
fl = @_list.getBoundingClientRect()
|
|
63
|
-
if fl.bottom > window.innerHeight
|
|
64
|
-
@_list.style.top = "#{tr.top - fl.height - 2}px"
|
|
65
|
-
|
|
66
|
-
isDisabled: (item) -> item?.hasAttribute?('data-disabled')
|
|
67
|
-
|
|
68
|
-
selectIndex: (idx) ->
|
|
69
|
-
item = @getItems()[idx]
|
|
70
|
-
return unless item
|
|
71
|
-
return if @isDisabled(item)
|
|
72
|
-
val = item.dataset.value ?? item.textContent.trim()
|
|
73
|
-
@query = val
|
|
74
|
-
@emit 'select', val
|
|
75
|
-
@close()
|
|
76
|
-
@_input?.blur()
|
|
77
|
-
|
|
78
|
-
_nextEnabled: (from, dir) ->
|
|
79
|
-
opts = @getItems()
|
|
80
|
-
len = opts.length
|
|
81
|
-
i = from
|
|
82
|
-
loop len
|
|
83
|
-
i = (i + dir) %% len
|
|
84
|
-
return i unless @isDisabled(opts[i])
|
|
85
|
-
from
|
|
86
|
-
|
|
87
|
-
_onKeydown: (e) ->
|
|
88
|
-
len = @getItems().length
|
|
89
|
-
switch e.key
|
|
90
|
-
when 'ArrowDown'
|
|
91
|
-
e.preventDefault()
|
|
92
|
-
@openMenu() unless open
|
|
93
|
-
highlightedIndex = @_nextEnabled(highlightedIndex, 1)
|
|
94
|
-
@_scrollToItem()
|
|
95
|
-
when 'ArrowUp'
|
|
96
|
-
e.preventDefault()
|
|
97
|
-
highlightedIndex = @_nextEnabled(highlightedIndex, -1)
|
|
98
|
-
@_scrollToItem()
|
|
99
|
-
when 'Enter'
|
|
100
|
-
e.preventDefault()
|
|
101
|
-
if highlightedIndex >= 0
|
|
102
|
-
@selectIndex(highlightedIndex)
|
|
103
|
-
else if len is 1
|
|
104
|
-
@selectIndex(0)
|
|
105
|
-
when 'Escape'
|
|
106
|
-
e.preventDefault()
|
|
107
|
-
if open then @close() else @query = ''
|
|
108
|
-
when 'Tab'
|
|
109
|
-
@close()
|
|
110
|
-
|
|
111
|
-
~>
|
|
112
|
-
if open
|
|
113
|
-
onDown = (e) =>
|
|
114
|
-
root = @_content
|
|
115
|
-
unless root?.contains(e.target)
|
|
116
|
-
@close()
|
|
117
|
-
document.addEventListener 'mousedown', onDown
|
|
118
|
-
return -> document.removeEventListener 'mousedown', onDown
|
|
119
|
-
|
|
120
|
-
render
|
|
121
|
-
. ref: "_content", $open: open?!
|
|
122
|
-
|
|
123
|
-
# Text input
|
|
124
|
-
. style: "position:relative;display:inline-flex;align-items:center"
|
|
125
|
-
input ref: "_input", role: "combobox"
|
|
126
|
-
type: "text"
|
|
127
|
-
autocomplete: "off"
|
|
128
|
-
aria-expanded: !!open
|
|
129
|
-
aria-haspopup: "listbox"
|
|
130
|
-
aria-autocomplete: "list"
|
|
131
|
-
aria-controls: if open then _listId else undefined
|
|
132
|
-
aria-activedescendant: if highlightedIndex >= 0 then "#{_listId}-#{highlightedIndex}" else undefined
|
|
133
|
-
$disabled: @disabled?!
|
|
134
|
-
disabled: @disabled
|
|
135
|
-
placeholder: @placeholder
|
|
136
|
-
value: @query
|
|
137
|
-
@keydown: @_onKeydown
|
|
138
|
-
if @query
|
|
139
|
-
button aria-label: "Clear", $clear: true, @click: @clear
|
|
140
|
-
"✕"
|
|
141
|
-
|
|
142
|
-
# Listbox — conditionally rendered (like Select)
|
|
143
|
-
if open and @items.length > 0
|
|
144
|
-
div ref: "_list", id: _listId, role: "listbox", $open: true
|
|
145
|
-
style: "position:fixed"
|
|
146
|
-
for item, idx in @items
|
|
147
|
-
div role: "option", tabindex: "-1", id: "#{_listId}-#{idx}"
|
|
148
|
-
$value: item
|
|
149
|
-
$highlighted: (idx is highlightedIndex)?!
|
|
150
|
-
@click: (=> @selectIndex(idx))
|
|
151
|
-
@mouseenter: (=> highlightedIndex = idx)
|
|
152
|
-
"#{item}"
|
|
153
|
-
if open and @items.length is 0 and @query
|
|
154
|
-
div role: "status", aria-live: "polite", $empty: true
|
|
155
|
-
"No results"
|
package/docs/ui/context-menu.rip
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# ContextMenu — accessible headless right-click menu
|
|
2
|
-
#
|
|
3
|
-
# Opens on contextmenu event (right-click) over the trigger area.
|
|
4
|
-
# Keyboard navigation matches Menu. Ships zero CSS.
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# ContextMenu @select: handleAction
|
|
8
|
-
# div $trigger: true
|
|
9
|
-
# p "Right-click this area"
|
|
10
|
-
# div $item: "cut", "Cut"
|
|
11
|
-
# div $item: "copy", "Copy"
|
|
12
|
-
# div $item: "paste", "Paste"
|
|
13
|
-
|
|
14
|
-
export ContextMenu = component
|
|
15
|
-
@disabled := false
|
|
16
|
-
|
|
17
|
-
open := false
|
|
18
|
-
highlightedIndex := -1
|
|
19
|
-
posX := 0
|
|
20
|
-
posY := 0
|
|
21
|
-
|
|
22
|
-
_menuItems ~=
|
|
23
|
-
return [] unless @_slot
|
|
24
|
-
Array.from(@_slot.querySelectorAll('[data-item]') or [])
|
|
25
|
-
|
|
26
|
-
_triggerEl ~=
|
|
27
|
-
return null unless @_slot
|
|
28
|
-
@_slot.querySelector('[data-trigger]')
|
|
29
|
-
|
|
30
|
-
_onContextMenu: (e) ->
|
|
31
|
-
return if @disabled
|
|
32
|
-
e.preventDefault()
|
|
33
|
-
posX = e.clientX
|
|
34
|
-
posY = e.clientY
|
|
35
|
-
open = true
|
|
36
|
-
highlightedIndex = 0
|
|
37
|
-
requestAnimationFrame =>
|
|
38
|
-
@_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
|
|
39
|
-
|
|
40
|
-
close: ->
|
|
41
|
-
open = false
|
|
42
|
-
highlightedIndex = -1
|
|
43
|
-
|
|
44
|
-
selectIndex: (idx) ->
|
|
45
|
-
item = _menuItems[idx]
|
|
46
|
-
return unless item
|
|
47
|
-
return if item.dataset.disabled?
|
|
48
|
-
@emit 'select', item.dataset.item
|
|
49
|
-
@close()
|
|
50
|
-
|
|
51
|
-
_onKeydown: (e) ->
|
|
52
|
-
len = _menuItems.length
|
|
53
|
-
return unless len
|
|
54
|
-
switch e.key
|
|
55
|
-
when 'ArrowDown'
|
|
56
|
-
e.preventDefault()
|
|
57
|
-
highlightedIndex = (highlightedIndex + 1) %% len
|
|
58
|
-
@_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
|
|
59
|
-
when 'ArrowUp'
|
|
60
|
-
e.preventDefault()
|
|
61
|
-
highlightedIndex = (highlightedIndex - 1) %% len
|
|
62
|
-
@_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
|
|
63
|
-
when 'Home'
|
|
64
|
-
e.preventDefault()
|
|
65
|
-
highlightedIndex = 0
|
|
66
|
-
@_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
|
|
67
|
-
when 'End'
|
|
68
|
-
e.preventDefault()
|
|
69
|
-
highlightedIndex = len - 1
|
|
70
|
-
@_list?.querySelectorAll('[role="menuitem"]')[len - 1]?.focus()
|
|
71
|
-
when 'Enter', ' '
|
|
72
|
-
e.preventDefault()
|
|
73
|
-
@selectIndex(highlightedIndex)
|
|
74
|
-
when 'Escape', 'Tab'
|
|
75
|
-
e.preventDefault() if e.key is 'Escape'
|
|
76
|
-
@close()
|
|
77
|
-
|
|
78
|
-
~>
|
|
79
|
-
if open
|
|
80
|
-
onDown = (e) =>
|
|
81
|
-
unless @_list?.contains(e.target)
|
|
82
|
-
@close()
|
|
83
|
-
document.addEventListener 'mousedown', onDown
|
|
84
|
-
return -> document.removeEventListener 'mousedown', onDown
|
|
85
|
-
|
|
86
|
-
render
|
|
87
|
-
. @contextmenu: @_onContextMenu
|
|
88
|
-
|
|
89
|
-
. ref: "_slot", style: "display:none"
|
|
90
|
-
slot
|
|
91
|
-
|
|
92
|
-
if _triggerEl
|
|
93
|
-
. innerHTML: _triggerEl.innerHTML
|
|
94
|
-
|
|
95
|
-
if open
|
|
96
|
-
div ref: "_list", role: "menu", $open: true, @keydown: @_onKeydown
|
|
97
|
-
style: "position:fixed;left:#{posX}px;top:#{posY}px;z-index:50"
|
|
98
|
-
for item, idx in _menuItems
|
|
99
|
-
div role: "menuitem", tabindex: "-1"
|
|
100
|
-
$highlighted: (idx is highlightedIndex)?!
|
|
101
|
-
$disabled: item.dataset.disabled?!
|
|
102
|
-
$value: item.dataset.item
|
|
103
|
-
@click: (=> @selectIndex(idx))
|
|
104
|
-
@mouseenter: (=> highlightedIndex = idx)
|
|
105
|
-
= item.textContent
|