turbo_boost-elements 0.0.8 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,177 @@
1
+ import ToggleElement, { busyDuration } from '../toggle_element'
2
+ import Devtool from './devtool'
3
+
4
+ export default class ToggleTriggerElement extends ToggleElement {
5
+ connectedCallback () {
6
+ super.connectedCallback()
7
+
8
+ if (this.targetElement)
9
+ this.targetElement.setAttribute('aria-labeledby', this.id)
10
+
11
+ const { start: commandStartEvent } = TurboBoost.Commands.events
12
+ this.commandStartHandler = this.onCommandStart.bind(this)
13
+ this.addEventListener(commandStartEvent, this.commandStartHandler)
14
+
15
+ const { before: beforeInvokeEvent } = TurboBoost.Streams.invokeEvents
16
+ this.beforeInvokeHandler = this.onBeforeInvoke.bind(this)
17
+ addEventListener(beforeInvokeEvent, this.beforeInvokeHandler)
18
+
19
+ // fires after receiving the toggle morph Turbo Stream but before it is executed
20
+ // this.addEventListener(TurboBoost.Commands.events.success, event => {
21
+ // // TODO: imlement cache, this.targetElement.cacheHTML()
22
+ // })
23
+
24
+ this.initializeDevtool()
25
+ }
26
+
27
+ disconnectedCallback () {
28
+ const { start: commandStartEvent } = TurboBoost.Commands.events
29
+ this.removeEventListener(commandStartEvent, this.commandStartHandler)
30
+
31
+ const { before: beforeInvokeEvent } = TurboBoost.Streams.invokeEvents
32
+ removeEventListener(beforeInvokeEvent, this.beforeInvokeHandler)
33
+
34
+ this.devtool.hide({ active: false })
35
+ this.devtool.unregisterEventListeners()
36
+ delete this.devtool
37
+ }
38
+
39
+ initializeDevtool () {
40
+ const mouseenter = () => this.devtool.show()
41
+
42
+ addEventListener('turbo-boost:devtools-start', () => {
43
+ this.devtool = new Devtool(this)
44
+ this.addEventListener('mouseenter', mouseenter)
45
+ })
46
+
47
+ addEventListener('turbo-boost:devtools-stop', () => {
48
+ this.removeEventListener('mouseenter', mouseenter)
49
+ this.devtool.hide({ active: false })
50
+ this.devtool.unregisterEventListeners()
51
+ delete this.devtool
52
+ })
53
+
54
+ this.dispatchEvent(
55
+ new CustomEvent('turbo-boost:devtools-connect', { bubbles: true })
56
+ )
57
+ }
58
+
59
+ hideDevtool () {
60
+ if (this.devtool) this.devtool.hide({ active: false })
61
+ }
62
+
63
+ onCommandStart (event) {
64
+ this.targetElement.setAttribute('aria-labeledby', this.id)
65
+ this.targetElement.collapseMatches()
66
+ this.targetElement.busy = true
67
+ this.busy = true
68
+ // TODO: implement cache - this.targetElement.renderCachedHTML()
69
+ }
70
+
71
+ onBeforeInvoke (event) {
72
+ if (event.detail.method !== 'morph') return
73
+ if (event.target.id !== this.morphs) return
74
+
75
+ // ensure the busy element is shown long enough for a good user experience
76
+ // we accomplish this by modifying the event.detail with invoke instructions i.e. { delay }
77
+ // SEE: the TurboBoost Streams library for details on how this works
78
+ const duration = Date.now() - this.busyStartedAt
79
+ let delay = busyDuration - duration
80
+ if (delay < 10) delay = 10
81
+ event.detail.invoke = { delay }
82
+
83
+ // runs before the morph is executed
84
+ setTimeout(() => {
85
+ this.busy = false
86
+ this.targetElement.busy = false
87
+ this.morphToggleElements.forEach(el => (el.busy = false))
88
+ this.expanded = !this.expanded
89
+ }, delay - 10)
90
+
91
+ // runs after the morph is executed
92
+ setTimeout(() => {
93
+ if (this.expanded) this.targetElement.focus()
94
+ }, delay + 10)
95
+ }
96
+
97
+ // a list of views shared between the trigger and target
98
+ get sharedViews () {
99
+ if (!this.targetElement) return []
100
+ if (!this.targetElement.viewStack) return []
101
+ const reducer = (memo, view) => {
102
+ if (this.targetElement.viewStack.includes(view)) memo.push(view)
103
+ return memo
104
+ }
105
+ return this.viewStack.reduce(reducer.bind(this), [])
106
+ }
107
+
108
+ // the partial to render
109
+ get renders () {
110
+ return this.getAttribute('renders')
111
+ }
112
+
113
+ // the renderered partial's top wrapping dom_id
114
+ get morphs () {
115
+ return this.getAttribute('morphs')
116
+ }
117
+
118
+ // the morph element
119
+ get morphElement () {
120
+ if (!this.morphs) return null
121
+ return document.getElementById(this.morphs)
122
+ }
123
+
124
+ // all toggle elements contained by the `morphElement`
125
+ get morphToggleElements () {
126
+ return Array.from(
127
+ this.morphElement.querySelectorAll(
128
+ 'turbo-boost-toggle-trigger,turbo-boost-toggle-target'
129
+ )
130
+ )
131
+ }
132
+
133
+ // the target's dom_id
134
+ get controls () {
135
+ return this.getAttribute('aria-controls')
136
+ }
137
+
138
+ // the target element
139
+ get targetElement () {
140
+ if (!this.controls) return null
141
+ return document.getElementById(this.controls)
142
+ }
143
+
144
+ get collapseSelector () {
145
+ return this.getAttribute('collapse-selector')
146
+ }
147
+
148
+ get focusSelector () {
149
+ return (
150
+ this.getAttribute('focus-selector') ||
151
+ this.targetElement.getAttribute('focus-selector')
152
+ )
153
+ }
154
+
155
+ // indicates if the toggle state should be remembered across requests
156
+ get remember () {
157
+ return this.getAttribute('remember') === 'true'
158
+ }
159
+
160
+ set remember (value) {
161
+ return this.setAttribute('remember', !!value)
162
+ }
163
+
164
+ // indicates if the target is expanded
165
+ get expanded () {
166
+ return this.getAttribute('aria-expanded') === 'true'
167
+ }
168
+
169
+ set expanded (value) {
170
+ this.setAttribute('aria-expanded', !!value)
171
+ }
172
+
173
+ // indicates if the target is expanded
174
+ get collapsed () {
175
+ return !this.expanded
176
+ }
177
+ }
@@ -1,9 +1,9 @@
1
1
  export default class TurboBoostElement extends HTMLElement {
2
- constructor () {
2
+ constructor (html) {
3
3
  super()
4
4
  this.devtool = 'unknown'
5
5
  this.attachShadow({ mode: 'open' })
6
- this.shadowRoot.innerHTML = '<slot></slot>'
6
+ this.shadowRoot.innerHTML = html || '<slot></slot>'
7
7
  }
8
8
 
9
9
  connectedCallback () {
@@ -54,10 +54,17 @@ export function removeHighlight (element) {
54
54
  export function coordinates (element) {
55
55
  if (!element) return {}
56
56
  const rect = element.getBoundingClientRect()
57
- return {
58
- left: rect.left + window.scrollX,
59
- top: rect.top + window.scrollY,
60
- width: element.offsetWidth,
61
- height: element.offsetHeight
62
- }
57
+ const width = element.offsetWidth
58
+ const height = element.offsetHeight
59
+ const top = rect.top + window.scrollY
60
+ const left = rect.left + window.scrollX
61
+ const right = left + width
62
+ const bottom = top + height
63
+ return { top, left, right, bottom, width, height }
64
+ }
65
+
66
+ export function attempt (callback) {
67
+ try {
68
+ callback()
69
+ } catch {}
63
70
  }
@@ -10,6 +10,7 @@ module TurboBoost::Elements
10
10
  end
11
11
 
12
12
  class Engine < ::Rails::Engine
13
+ isolate_namespace TurboBoost::Elements
13
14
  config.turbo_boost_elements = ActiveSupport::OrderedOptions.new
14
15
 
15
16
  ActiveSupport.on_load(:action_controller_base) do
@@ -1,11 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TurboBoost::Elements::TagBuilders::BaseTagBuilder
4
- attr_reader :view_context
5
- delegate :content_tag, :turbo_boost, to: :view_context
4
+ attr_reader :controller_pack
6
5
 
7
6
  def initialize(view_context)
8
7
  @view_context = view_context
8
+ @controller_pack = view_context.turbo_boost # TurboBoost::Commands::ControllerPack
9
+ end
10
+
11
+ def render_tag(name, loading: :eager, **kwargs, &block)
12
+ options = kwargs.select { |_, value| value.present? }
13
+ options.transform_keys! { |key| key.to_s.dasherize }
14
+
15
+ loading = :eager unless loading == :lazy
16
+ if loading == :eager
17
+ view_context.tag.public_send(name.to_sym, **options, &block)
18
+ else
19
+ view_context.tag.public_send(name.to_sym, **options) {}
20
+ end
9
21
  end
10
22
 
11
23
  def view_stack
@@ -15,4 +27,8 @@ class TurboBoost::Elements::TagBuilders::BaseTagBuilder
15
27
  memo << location.path[(location.path.index(prefix) + prefix.length)..]
16
28
  end
17
29
  end
30
+
31
+ protected
32
+
33
+ attr_reader :view_context
18
34
  end
@@ -3,6 +3,17 @@
3
3
  require_relative "base_tag_builder"
4
4
 
5
5
  class TurboBoost::Elements::TagBuilders::ToggleTagsBuilder < TurboBoost::Elements::TagBuilders::BaseTagBuilder
6
+ def busy_tag(**kwargs, &block)
7
+ kwargs[:slot] = "busy"
8
+ render_tag("turbo-boost", loading: :eager, **kwargs, &block)
9
+ end
10
+
11
+ # def content_tag(loading: :eager, **kwargs, &block)
12
+ # kwargs[:slot] = "content"
13
+ # kwargs[:hidden] = true
14
+ # render_tag("turbo-boost", loading: loading, **kwargs, &block)
15
+ # end
16
+
6
17
  def trigger_tag(
7
18
  renders:, # REQUIRED, the partial path to render
8
19
  morphs:, # REQUIRED, `dom_id` of the partial's outermost containing element
@@ -16,7 +27,7 @@ class TurboBoost::Elements::TagBuilders::ToggleTagsBuilder < TurboBoost::Element
16
27
  &block # a Ruby block that emits this trigger's content
17
28
  )
18
29
  kwargs = kwargs.with_indifferent_access
19
- kwargs[:id] ||= "#{controls}-toggle-trigger"
30
+ kwargs[:id] ||= "#{controls}-toggle-trigger-#{SecureRandom.hex(6)}"
20
31
 
21
32
  # command
22
33
  kwargs[:data] ||= {}
@@ -39,10 +50,7 @@ class TurboBoost::Elements::TagBuilders::ToggleTagsBuilder < TurboBoost::Element
39
50
  kwargs[:focus_selector] = focus_selector
40
51
  kwargs[:remember] = !!remember
41
52
 
42
- args = kwargs.select { |_, value| value.present? }
43
- args = args.transform_keys(&:dasherize)
44
-
45
- content_tag("turbo-boost-toggle-trigger", nil, args, &block)
53
+ render_tag("turbo-boost-toggle-trigger", loading: :eager, **kwargs, &block)
46
54
  end
47
55
 
48
56
  def target_tag(
@@ -68,16 +76,12 @@ class TurboBoost::Elements::TagBuilders::ToggleTagsBuilder < TurboBoost::Element
68
76
  # rendering
69
77
  kwargs[:view_stack] = view_stack.to_json if Rails.env.development?
70
78
 
71
- args = kwargs.select { |_, value| value.present? }
72
- if expanded || target_expanded?(id)
73
- content_tag("turbo-boost-toggle-target", nil, args.transform_keys!(&:dasherize), &block)
74
- else
75
- content_tag("turbo-boost-toggle-target", nil, args.transform_keys!(&:dasherize))
76
- end
79
+ loading = (expanded || target_expanded?(id)) ? :eager : :lazy
80
+ render_tag("turbo-boost-toggle-target", loading: loading, **kwargs, &block)
77
81
  end
78
82
 
79
83
  def target_expanded?(dom_id)
80
- !!turbo_boost.state[dom_id]
84
+ !!controller_pack.state[dom_id]
81
85
  end
82
86
 
83
87
  def target_collapsed?(dom_id)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TurboBoost
4
4
  module Elements
5
- VERSION = "0.0.8"
5
+ VERSION = "0.0.10"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_boost-elements
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Hopkins (hopsoft)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-19 00:00:00.000000000 Z
11
+ date: 2023-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 0.0.9
47
+ version: 0.0.11
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 0.0.9
54
+ version: 0.0.11
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: magic_frozen_string_literal
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -143,12 +143,13 @@ files:
143
143
  - app/javascript/devtools/elements/tooltip_element.js
144
144
  - app/javascript/devtools/index.js
145
145
  - app/javascript/devtools/supervisor.js
146
- - app/javascript/devtools/toggle.js
147
146
  - app/javascript/elements/index.js
148
- - app/javascript/elements/toggle_target_element.js
149
- - app/javascript/elements/toggle_target_focus.js
150
- - app/javascript/elements/toggle_trigger_element.js
151
- - app/javascript/elements/turbo_boost_element.js
147
+ - app/javascript/elements/toggle_elements/target_element/focus.js
148
+ - app/javascript/elements/toggle_elements/target_element/index.js
149
+ - app/javascript/elements/toggle_elements/toggle_element/index.js
150
+ - app/javascript/elements/toggle_elements/trigger_element/devtool.js
151
+ - app/javascript/elements/toggle_elements/trigger_element/index.js
152
+ - app/javascript/elements/turbo_boost_element/index.js
152
153
  - app/javascript/index.js
153
154
  - app/javascript/utils/dom.js
154
155
  - lib/turbo_boost/elements.rb
@@ -1,84 +0,0 @@
1
- import TurboBoostElement from './turbo_boost_element'
2
- import './toggle_target_focus'
3
-
4
- export default class ToggleTargetElement extends TurboBoostElement {
5
- connectedCallback () {
6
- super.connectedCallback()
7
-
8
- this.addEventListener('mouseenter', () =>
9
- clearTimeout(this.collapseTimeout)
10
- )
11
-
12
- this.collapseOn.forEach(name =>
13
- this.addEventListener(name, () => this.collapse())
14
- )
15
- }
16
-
17
- // TODO: get cached content working properly
18
- // perhaps use a mechanic other than morph
19
-
20
- cacheHTML () {
21
- // this.cachedHTML = this.innerHTML
22
- }
23
-
24
- renderCachedHTML () {
25
- // if (!this.cachedHTML) return
26
- // this.innerHTML = this.cachedHTML
27
- }
28
-
29
- collapse (delay = 250) {
30
- if (delay > 0) {
31
- clearTimeout(this.collapseTimeout)
32
- return (this.collapseTimeout = setTimeout(() => this.collapse(0), delay))
33
- }
34
-
35
- this.innerHTML = ''
36
- try {
37
- this.currentTriggerElement.expanded = false
38
- this.currentTriggerElement.hideDevtool()
39
- } catch {}
40
- }
41
-
42
- collapseMatches () {
43
- document.querySelectorAll(this.collapseSelector).forEach(el => {
44
- if (el === this) return
45
- if (el.collapse) el.collapse(0)
46
- })
47
- }
48
-
49
- get collapseSelector () {
50
- if (
51
- this.currentTriggerElement &&
52
- this.currentTriggerElement.collapseSelector
53
- )
54
- return this.currentTriggerElement.collapseSelector
55
- return this.getAttribute('collapse-selector')
56
- }
57
-
58
- focus () {
59
- clearTimeout(this.focusTimeout)
60
- this.focusTimeout = setTimeout(() => {
61
- if (this.focusElement) this.focusElement.focus()
62
- }, 50)
63
- }
64
-
65
- get focusSelector () {
66
- if (this.currentTriggerElement && this.currentTriggerElement.focusSelector)
67
- return this.currentTriggerElement.focusSelector
68
- return this.getAttribute('focus-selector')
69
- }
70
-
71
- get focusElement () {
72
- return this.querySelector(this.focusSelector)
73
- }
74
-
75
- get labeledBy () {
76
- return this.getAttribute('aria-labeledby')
77
- }
78
-
79
- get collapseOn () {
80
- const value = this.getAttribute('collapse-on')
81
- if (!value) return []
82
- return JSON.parse(value)
83
- }
84
- }
@@ -1,122 +0,0 @@
1
- import TurboBoostElement from './turbo_boost_element'
2
- import DevtoolSupervisor from '../devtools/supervisor'
3
- import ToggleDevtool from '../devtools/toggle'
4
-
5
- export default class ToggleTriggerElement extends TurboBoostElement {
6
- connectedCallback () {
7
- super.connectedCallback()
8
-
9
- if (this.targetElement) {
10
- this.targetElement.setAttribute('aria-labeledby', this.id)
11
- }
12
-
13
- this.addEventListener(TurboBoost.Commands.events.start, () => {
14
- this.busy = true
15
- this.targetElement.currentTriggerElement = this
16
- this.targetElement.renderCachedHTML()
17
- })
18
-
19
- this.addEventListener(TurboBoost.Commands.events.success, () => {
20
- this.busy = false
21
- this.targetElement.focus()
22
- this.targetElement.collapseMatches()
23
- this.targetElement.cacheHTML()
24
- })
25
-
26
- this.addEventListener(
27
- TurboBoost.Commands.events.finish,
28
- () => (this.busy = false)
29
- )
30
-
31
- this.initializeDevtool()
32
- }
33
-
34
- initializeDevtool () {
35
- const mouseenter = () => this.devtool.show()
36
-
37
- addEventListener('turbo-boost:devtools-start', () => {
38
- this.devtool = new ToggleDevtool(this)
39
- this.addEventListener('mouseenter', mouseenter)
40
- })
41
-
42
- addEventListener('turbo-boost:devtools-stop', () => {
43
- this.removeEventListener('mouseenter', mouseenter)
44
- delete this.devtool
45
- })
46
-
47
- if (DevtoolSupervisor.started) DevtoolSupervisor.restart()
48
- }
49
-
50
- hideDevtool () {
51
- if (this.devtool) this.devtool.hide(true)
52
- }
53
-
54
- // a list of views shared between the trigger and target
55
- get sharedViews () {
56
- if (!this.targetElement) return []
57
- if (!this.targetElement.viewStack) return []
58
- const reducer = (memo, view) => {
59
- if (this.targetElement.viewStack.includes(view)) memo.push(view)
60
- return memo
61
- }
62
- return this.viewStack.reduce(reducer.bind(this), [])
63
- }
64
-
65
- // the partial to render
66
- get renders () {
67
- return this.getAttribute('renders')
68
- }
69
-
70
- // the renderered partial's top wrapping dom_id
71
- get morphs () {
72
- return this.getAttribute('morphs')
73
- }
74
-
75
- // the morph element
76
- get morphElement () {
77
- if (!this.morphs) return null
78
- return document.getElementById(this.morphs)
79
- }
80
-
81
- // the target's dom_id
82
- get controls () {
83
- return this.getAttribute('aria-controls')
84
- }
85
-
86
- // the target element
87
- get targetElement () {
88
- if (!this.controls) return null
89
- return document.getElementById(this.controls)
90
- }
91
-
92
- get collapseSelector () {
93
- return this.getAttribute('collapse-selector')
94
- }
95
-
96
- get focusSelector () {
97
- return this.getAttribute('focus-selector')
98
- }
99
-
100
- // indicates if the target is expanded
101
- get expanded () {
102
- return this.getAttribute('aria-expanded') === 'true'
103
- }
104
-
105
- set expanded (value) {
106
- this.setAttribute('aria-expanded', !!value)
107
- }
108
-
109
- // indicates if the target is expanded
110
- get collapsed () {
111
- return !this.expanded
112
- }
113
-
114
- // indicates if an rpc call is active/busy
115
- get busy () {
116
- return this.getAttribute('busy') === 'true'
117
- }
118
-
119
- set busy (value) {
120
- this.setAttribute('busy', !!value)
121
- }
122
- }