rip-lang 3.15.4 → 3.16.1

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.
Files changed (112) hide show
  1. package/README.md +6 -4
  2. package/bin/rip +167 -12
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-APP.md +808 -0
  5. package/docs/RIP-DUCKDB.md +477 -0
  6. package/docs/RIP-INTRO.md +396 -0
  7. package/docs/RIP-LANG.md +59 -5
  8. package/docs/RIP-SCHEMA.md +191 -8
  9. package/docs/RIP-TYPES.md +74 -103
  10. package/docs/demo/README.md +4 -3
  11. package/docs/dist/rip.js +3627 -1470
  12. package/docs/dist/rip.min.js +671 -244
  13. package/docs/dist/rip.min.js.br +0 -0
  14. package/docs/example/index.json +7 -7
  15. package/docs/example/index.json.br +0 -0
  16. package/docs/extensions/duckdb/manifest.json +1 -1
  17. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  18. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  19. package/docs/extensions/vscode/print/index.html +2 -1
  20. package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
  21. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  22. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  23. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  24. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  25. package/docs/ui/bundle.json +61 -0
  26. package/docs/ui/bundle.json.br +0 -0
  27. package/docs/ui/hljs-rip.js +0 -7
  28. package/docs/ui/index.css +66 -23
  29. package/docs/ui/index.html +6 -6
  30. package/package.json +9 -3
  31. package/rip-loader.js +64 -2
  32. package/src/AGENTS.md +63 -36
  33. package/src/browser.js +96 -14
  34. package/src/compiler.js +960 -143
  35. package/src/components.js +794 -88
  36. package/src/{types-emit.js → dts.js} +181 -71
  37. package/src/grammar/README.md +1 -1
  38. package/src/grammar/grammar.rip +111 -97
  39. package/src/lexer.js +132 -18
  40. package/src/parser.js +203 -205
  41. package/src/repl.js +74 -6
  42. package/src/schema/runtime-orm.js +168 -4
  43. package/src/schema/runtime-validate.js +146 -2
  44. package/src/schema/runtime.generated.js +314 -6
  45. package/src/schema/schema.js +5 -5
  46. package/src/sourcemaps.js +277 -1
  47. package/src/stdlib.js +253 -0
  48. package/src/typecheck.js +2023 -106
  49. package/src/types.js +127 -7
  50. package/docs/ui/accordion.rip +0 -103
  51. package/docs/ui/alert-dialog.rip +0 -53
  52. package/docs/ui/autocomplete.rip +0 -115
  53. package/docs/ui/avatar.rip +0 -37
  54. package/docs/ui/badge.rip +0 -15
  55. package/docs/ui/breadcrumb.rip +0 -47
  56. package/docs/ui/button-group.rip +0 -26
  57. package/docs/ui/button.rip +0 -23
  58. package/docs/ui/card.rip +0 -25
  59. package/docs/ui/carousel.rip +0 -110
  60. package/docs/ui/checkbox-group.rip +0 -61
  61. package/docs/ui/checkbox.rip +0 -33
  62. package/docs/ui/collapsible.rip +0 -50
  63. package/docs/ui/combobox.rip +0 -130
  64. package/docs/ui/context-menu.rip +0 -88
  65. package/docs/ui/date-picker.rip +0 -206
  66. package/docs/ui/dialog.rip +0 -60
  67. package/docs/ui/drawer.rip +0 -58
  68. package/docs/ui/editable-value.rip +0 -82
  69. package/docs/ui/field.rip +0 -53
  70. package/docs/ui/fieldset.rip +0 -22
  71. package/docs/ui/form.rip +0 -39
  72. package/docs/ui/grid.rip +0 -901
  73. package/docs/ui/input-group.rip +0 -28
  74. package/docs/ui/input.rip +0 -36
  75. package/docs/ui/label.rip +0 -16
  76. package/docs/ui/menu.rip +0 -134
  77. package/docs/ui/menubar.rip +0 -151
  78. package/docs/ui/meter.rip +0 -36
  79. package/docs/ui/multi-select.rip +0 -203
  80. package/docs/ui/native-select.rip +0 -33
  81. package/docs/ui/nav-menu.rip +0 -126
  82. package/docs/ui/number-field.rip +0 -162
  83. package/docs/ui/otp-field.rip +0 -89
  84. package/docs/ui/pagination.rip +0 -123
  85. package/docs/ui/popover.rip +0 -93
  86. package/docs/ui/preview-card.rip +0 -75
  87. package/docs/ui/progress.rip +0 -25
  88. package/docs/ui/radio-group.rip +0 -57
  89. package/docs/ui/resizable.rip +0 -123
  90. package/docs/ui/scroll-area.rip +0 -145
  91. package/docs/ui/select.rip +0 -151
  92. package/docs/ui/separator.rip +0 -17
  93. package/docs/ui/skeleton.rip +0 -22
  94. package/docs/ui/slider.rip +0 -165
  95. package/docs/ui/spinner.rip +0 -17
  96. package/docs/ui/table.rip +0 -27
  97. package/docs/ui/tabs.rip +0 -113
  98. package/docs/ui/textarea.rip +0 -48
  99. package/docs/ui/toast.rip +0 -87
  100. package/docs/ui/toggle-group.rip +0 -71
  101. package/docs/ui/toggle.rip +0 -24
  102. package/docs/ui/toolbar.rip +0 -38
  103. package/docs/ui/tooltip.rip +0 -85
  104. package/src/app.rip +0 -1571
  105. package/src/sourcemap-merge.js +0 -287
  106. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  107. /package/docs/demo/{components → routes}/about.rip +0 -0
  108. /package/docs/demo/{components → routes}/card.rip +0 -0
  109. /package/docs/demo/{components → routes}/counter.rip +0 -0
  110. /package/docs/demo/{components → routes}/index.rip +0 -0
  111. /package/docs/demo/{components → routes}/todos.rip +0 -0
  112. /package/src/schema/{dts-emit.js → dts.js} +0 -0
@@ -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" | "vertical" := "horizontal"
22
- @loop:: boolean := false
23
- @autoplay:: boolean := false
24
- @interval:: number := 4000
25
- @label:: string := "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,61 +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:: any[] := []
15
- @disabled:: boolean := false
16
- @orientation:: "horizontal" | "vertical" := "vertical"
17
- @label:: string := ""
18
-
19
- _options ~=
20
- return [] unless @_slot
21
- Array.from(@_slot.querySelectorAll('[data-value]') or [])
22
-
23
- _toggle: (val) ->
24
- return if @disabled
25
- arr = if Array.isArray(@value) then [...@value] else []
26
- if val in arr
27
- arr = arr.filter (v) -> v isnt val
28
- else
29
- arr.push val
30
- @value = arr
31
- @emit 'change', @value
32
-
33
- onKeydown: (e) ->
34
- boxes = @_root?.querySelectorAll('[role="checkbox"]')
35
- return unless boxes?.length
36
- focused = Array.from(boxes).indexOf(document.activeElement)
37
- return if focused < 0
38
- len = boxes.length
39
- ARIA.rovingNav e, {
40
- next: => boxes[(focused + 1) %% len]?.focus()
41
- prev: => boxes[(focused - 1) %% len]?.focus()
42
- first: => boxes[0]?.focus()
43
- last: => boxes[len - 1]?.focus()
44
- }, 'both'
45
-
46
- render
47
- div ref: "_root", role: "group", aria-label: @label or undefined, aria-orientation: @orientation
48
- $orientation: @orientation
49
- $disabled: @disabled?!
50
-
51
- . ref: "_slot", style: "display:none"
52
- slot
53
-
54
- for opt, idx in _options
55
- button role: "checkbox", tabindex: (if idx is 0 then "0" else "-1")
56
- aria-checked: opt.dataset.value in @value
57
- $checked: (opt.dataset.value in @value)?!
58
- $disabled: @disabled?!
59
- $value: opt.dataset.value
60
- @click: (=> @_toggle(opt.dataset.value))
61
- = opt.textContent
@@ -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:: boolean := false
16
- @disabled:: boolean := false
17
- @indeterminate:: boolean := false
18
- @switch:: boolean := 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
@@ -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:: boolean := false
15
- @disabled:: boolean := 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
- if @disabled then trigger.setAttribute 'aria-disabled', true else trigger.removeAttribute 'aria-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
@@ -1,130 +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:: string := ""
14
- @items:: any[] := []
15
- @placeholder:: string := "Search..."
16
- @disabled:: boolean := false
17
- @autoHighlight:: boolean := 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: ->
44
- setTimeout => @close() unless @_content?.contains(document.activeElement), 0
45
-
46
- openMenu: ->
47
- open = true
48
- highlightedIndex = -1
49
- @_input?.focus()
50
-
51
- close: ->
52
- open = false
53
- highlightedIndex = -1
54
- @_input?.focus()
55
-
56
- _applyPlacement: ->
57
- ARIA.position @_content, @_list, placement: 'bottom start', offset: 2, matchWidth: true
58
-
59
- isDisabled: (item) -> item?.hasAttribute?('data-disabled')
60
-
61
- selectIndex: (idx) ->
62
- item = @getItems()[idx]
63
- return unless item
64
- return if @isDisabled(item)
65
- val = item.dataset.value ?? item.textContent.trim()
66
- @query = val
67
- @emit 'select', val
68
- @close()
69
-
70
- _nextEnabled: (from, dir) ->
71
- opts = @getItems()
72
- len = opts.length
73
- i = from
74
- loop len
75
- i = (i + dir) %% len
76
- return i unless @isDisabled(opts[i])
77
- from
78
-
79
- _onKeydown: (e) ->
80
- len = @getItems().length
81
- ARIA.listNav e,
82
- next: => @openMenu() unless open; highlightedIndex = @_nextEnabled(highlightedIndex, 1); @_scrollToItem()
83
- prev: => @openMenu() unless open; highlightedIndex = @_nextEnabled(highlightedIndex, -1); @_scrollToItem()
84
- first: => highlightedIndex = 0; @_scrollToItem()
85
- last: => highlightedIndex = len - 1; @_scrollToItem()
86
- select: => if highlightedIndex >= 0 then @selectIndex(highlightedIndex) else if len is 1 then @selectIndex(0)
87
- dismiss: => if open then @close() else @query = ''
88
- tab: => @close()
89
-
90
- ~>
91
- if @_list
92
- @_list.setAttribute 'popover', 'auto'
93
- @_applyPlacement()
94
- ARIA.bindPopover open, (=> @_list), ((isOpen) => open = isOpen), (=> @_input)
95
-
96
- render
97
- . ref: "_content", $open: open?!
98
-
99
- # Text input
100
- . style: "position:relative;display:inline-flex;align-items:center"
101
- input ref: "_input", role: "combobox"
102
- type: "text"
103
- autocomplete: "off"
104
- aria-expanded: !!open
105
- aria-haspopup: "listbox"
106
- aria-autocomplete: "list"
107
- aria-controls: if open then _listId else undefined
108
- aria-activedescendant: if highlightedIndex >= 0 then "#{_listId}-#{highlightedIndex}" else undefined
109
- $disabled: @disabled?!
110
- disabled: @disabled
111
- placeholder: @placeholder
112
- value: @query
113
- @keydown: @_onKeydown
114
- if @query
115
- button aria-label: "Clear", $clear: true, @click: @clear
116
- "✕"
117
-
118
- # Listbox
119
- div ref: "_list", id: _listId, role: "listbox", style: "position:fixed;margin:0;inset:auto"
120
- $open: open?!
121
- for item, idx in @items
122
- div role: "option", tabindex: "-1", id: "#{_listId}-#{idx}"
123
- $value: item
124
- $highlighted: (idx is highlightedIndex)?!
125
- @click: (=> @selectIndex(idx))
126
- @mouseenter: (=> highlightedIndex = idx)
127
- "#{item}"
128
- if @items.length is 0 and @query
129
- div role: "status", aria-live: "polite", $empty: true
130
- "No results"
@@ -1,88 +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:: boolean := 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
- _focusItem: (idx) ->
52
- highlightedIndex = idx
53
- @_list?.querySelectorAll('[role="menuitem"]')[idx]?.focus()
54
-
55
- _onKeydown: (e) ->
56
- return unless _menuItems.length
57
- len = _menuItems.length
58
- ARIA.listNav e,
59
- next: => @_focusItem((highlightedIndex + 1) %% len)
60
- prev: => @_focusItem((highlightedIndex - 1) %% len)
61
- first: => @_focusItem(0)
62
- last: => @_focusItem(len - 1)
63
- select: => @selectIndex(highlightedIndex)
64
- dismiss: => @close()
65
- tab: => @close()
66
-
67
- ~> ARIA.popupDismiss open, (=> @_list), (=> @close())
68
-
69
- render
70
- . @contextmenu: @_onContextMenu
71
-
72
- . ref: "_slot", style: "display:none"
73
- slot
74
-
75
- if _triggerEl
76
- . innerHTML: _triggerEl.innerHTML
77
-
78
- if open
79
- div ref: "_list", role: "menu", $open: true, @keydown: @_onKeydown
80
- style: "position:fixed;left:#{posX}px;top:#{posY}px;z-index:50"
81
- for item, idx in _menuItems
82
- div role: "menuitem", tabindex: "-1"
83
- $highlighted: (idx is highlightedIndex)?!
84
- $disabled: item.dataset.disabled?!
85
- $value: item.dataset.item
86
- @click: (=> @selectIndex(idx))
87
- @mouseenter: (=> highlightedIndex = idx)
88
- = item.textContent
@@ -1,206 +0,0 @@
1
- # DatePicker — accessible headless date picker with calendar
2
- #
3
- # A popover calendar for selecting a single date or a date range.
4
- # Set @range to true for range selection (value becomes [from, to]).
5
- # Keyboard: Arrow keys navigate days, Enter selects, Escape closes.
6
- # Ships zero CSS — style entirely via attribute selectors in your stylesheet.
7
- #
8
- # Usage:
9
- # DatePicker value <=> selectedDate, placeholder: "Pick a date"
10
- # DatePicker value <=> dateRange, range: true
11
-
12
- dpFmt = (d) ->
13
- return '' unless d
14
- m = String(d.getMonth() + 1).padStart(2, '0')
15
- day = String(d.getDate()).padStart(2, '0')
16
- "#{m}/#{day}/#{d.getFullYear()}"
17
-
18
- dpParse = (str) ->
19
- return null unless str?.length is 10
20
- parts = str.split('/')
21
- return null unless parts.length is 3
22
- [m, d, y] = parts.map Number
23
- return null if isNaN(m) or isNaN(d) or isNaN(y)
24
- dt = new Date(y, m - 1, d)
25
- return null if dt.getMonth() isnt m - 1
26
- dt
27
-
28
- dpSameDay = (a, b) ->
29
- return false unless a and b
30
- a.getFullYear() is b.getFullYear() and a.getMonth() is b.getMonth() and a.getDate() is b.getDate()
31
-
32
- dpInRange = (day, from, to) ->
33
- return false unless day and from and to
34
- t = day.getTime()
35
- lo = Math.min(from.getTime(), to.getTime())
36
- hi = Math.max(from.getTime(), to.getTime())
37
- t >= lo and t <= hi
38
-
39
- export DatePicker = component
40
- @value:: any := null
41
- @placeholder:: string := "mm/dd/yyyy"
42
- @disabled:: boolean := false
43
- @range:: boolean := false
44
- @firstDayOfWeek:: number := 0
45
-
46
- open := false
47
- viewMonth := new Date()
48
- _rangeStart := null
49
- _hoveredDay := null
50
- _inputText := ''
51
- _id =! "dp-#{Math.random().toString(36).slice(2, 8)}"
52
-
53
- _dayNames =! ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
54
-
55
- _daysInView ~=
56
- yr = viewMonth.getFullYear()
57
- mo = viewMonth.getMonth()
58
- firstOfMonth = new Date(yr, mo, 1)
59
- startDay = firstOfMonth.getDay()
60
- offset = (startDay - @firstDayOfWeek + 7) %% 7
61
- daysInMonth = new Date(yr, mo + 1, 0).getDate()
62
- prevMonthDays = new Date(yr, mo, 0).getDate()
63
- dayList = []
64
- for n in [0...offset]
65
- dayList.push { date: new Date(yr, mo - 1, prevMonthDays - offset + n + 1), outside: true }
66
- for n in [1..daysInMonth]
67
- dayList.push { date: new Date(yr, mo, n), outside: false }
68
- trailing = (7 - dayList.length %% 7) %% 7
69
- for n in [1..trailing]
70
- dayList.push { date: new Date(yr, mo + 1, n), outside: true }
71
- dayList
72
-
73
- _displayText ~=
74
- if @range
75
- if Array.isArray(@value) and @value[0]
76
- from = dpFmt(@value[0])
77
- to = if @value[1] then dpFmt(@value[1]) else '...'
78
- "#{from} – #{to}"
79
- else
80
- @placeholder
81
- else
82
- if @value then dpFmt(@value) else @placeholder
83
-
84
- _monthLabel ~=
85
- viewMonth.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
86
-
87
- _today =! new Date()
88
-
89
- _prevMonth: ->
90
- viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() - 1, 1)
91
-
92
- _nextMonth: ->
93
- viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1)
94
-
95
- _selectDay: (day) ->
96
- return if @disabled
97
- if @range
98
- if _rangeStart and not dpSameDay(_rangeStart, day)
99
- from = if _rangeStart < day then _rangeStart else day
100
- to = if _rangeStart < day then day else _rangeStart
101
- @value = [from, to]
102
- _rangeStart = null
103
- @emit 'change', @value
104
- open = false
105
- else
106
- _rangeStart = day
107
- @value = [day, null]
108
- else
109
- @value = day
110
- @emit 'change', @value
111
- open = false
112
- _inputText = _displayText
113
-
114
- _onInputChange: (e) ->
115
- raw = e.target.value.replace(/[^\d\/]/g, '')
116
- if raw.length <= 10
117
- _inputText = raw
118
- if raw.length is 10
119
- dt = dpParse(raw)
120
- if dt
121
- @value = dt
122
- viewMonth = new Date(dt.getFullYear(), dt.getMonth(), 1)
123
- @emit 'change', @value
124
-
125
- toggle: ->
126
- return if @disabled
127
- if open then @close() else @openPicker()
128
-
129
- openPicker: ->
130
- open = true
131
- if @value and not @range
132
- viewMonth = new Date(@value.getFullYear(), @value.getMonth(), 1)
133
- else if @range and Array.isArray(@value) and @value[0]
134
- viewMonth = new Date(@value[0].getFullYear(), @value[0].getMonth(), 1)
135
- _inputText = _displayText
136
- requestAnimationFrame => @_position()
137
-
138
- close: ->
139
- open = false
140
- _rangeStart = null
141
- _hoveredDay = null
142
-
143
- _position: -> ARIA.positionBelow @_trigger, @_cal, 4, false
144
-
145
- _onKeydown: (e) ->
146
- switch e.key
147
- when 'Escape'
148
- e.preventDefault()
149
- @close()
150
- @_trigger?.focus()
151
- when 'Enter', ' '
152
- e.preventDefault()
153
- @toggle() unless open
154
-
155
- ~>
156
- if open
157
- onDown = (e) =>
158
- unless @_trigger?.contains(e.target) or @_cal?.contains(e.target)
159
- @close()
160
- document.addEventListener 'mousedown', onDown
161
- return -> document.removeEventListener 'mousedown', onDown
162
-
163
- render
164
- . $open: open?!, $disabled: @disabled?!, $range: @range?!
165
-
166
- # Trigger
167
- button ref: "_trigger", $trigger: true
168
- aria-haspopup: "dialog"
169
- aria-expanded: !!open
170
- disabled: @disabled
171
- @click: @toggle
172
- @keydown: @_onKeydown
173
- _displayText
174
-
175
- # Calendar dropdown
176
- if open
177
- div ref: "_cal", role: "dialog", aria-label: "Date picker", $calendar: true
178
- style: "position:fixed;z-index:50"
179
-
180
- # Month navigation
181
- . $header: true
182
- button $prev: true, aria-label: "Previous month", @click: @_prevMonth
183
- "‹"
184
- span $month-label: true
185
- _monthLabel
186
- button $next: true, aria-label: "Next month", @click: @_nextMonth
187
- "›"
188
-
189
- # Day-of-week headers
190
- . $weekdays: true
191
- for dayName in _dayNames
192
- span $weekday: true
193
- dayName
194
-
195
- # Day grid
196
- . $days: true, role: "grid"
197
- for entry, dIdx in _daysInView
198
- button role: "gridcell", tabindex: "-1"
199
- $outside: entry.outside?!
200
- $today: dpSameDay(entry.date, _today)?!
201
- $selected: (if @range then (Array.isArray(@value) and (dpSameDay(entry.date, @value[0]) or dpSameDay(entry.date, @value[1]))) else dpSameDay(entry.date, @value))?!
202
- $in-range: (if @range then (if _rangeStart then dpInRange(entry.date, _rangeStart, _hoveredDay or _rangeStart) else if Array.isArray(@value) and @value[0] and @value[1] then dpInRange(entry.date, @value[0], @value[1]) else false) else false)?!
203
- $range-start: (if @range and _rangeStart then dpSameDay(entry.date, _rangeStart) else false)?!
204
- @click: (=> @_selectDay(entry.date))
205
- @mouseenter: (=> _hoveredDay = entry.date)
206
- entry.date.getDate()