reflex_behaviors 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,178 @@
1
+ import { appendHTML, addHighlight, removeHighlight } from './dom'
2
+ import supervisor from './supervisor'
3
+
4
+ document.addEventListener('reflex-behaviors:devtools-start', () =>
5
+ supervisor.register('toggle', 'toggles<small>(trigger/target)</small>')
6
+ )
7
+
8
+ const triggerTooltipId = 'toggle-trigger-tooltip'
9
+ const targetTooltipId = 'toggle-target-tooltip'
10
+
11
+ function appendTooltip (id, title, content, options = {}) {
12
+ let { backgroundColor, color, emphaisColor, position } = options
13
+ color = color || 'white'
14
+ position = position || 'top'
15
+
16
+ appendHTML(`
17
+ <reflex-behaviors-devools-tooltip id="${id}" position="${position}" background-color="${backgroundColor}" color="${color}" emphasis-color="${emphaisColor}">
18
+ <div slot='title'>${title}</div>
19
+ ${content}
20
+ </reflex-behaviors-devools-tooltip>
21
+ `)
22
+ return document.getElementById(id)
23
+ }
24
+
25
+ export default class ToggleDevtool {
26
+ constructor (trigger) {
27
+ this.name = 'toggle'
28
+ this.reflex = trigger.dataset.turboReflex
29
+ this.trigger = trigger
30
+ this.target = trigger.target
31
+
32
+ document.addEventListener('reflex-behaviors:devtool-enable', event => {
33
+ const { name } = event.detail
34
+ if (name === this.name)
35
+ addHighlight(this.trigger, { color: 'red', offset: '2px' })
36
+ })
37
+
38
+ document.addEventListener('reflex-behaviors:devtool-disable', event => {
39
+ const { name } = event.detail
40
+ if (name === this.name) removeHighlight(this.trigger)
41
+ })
42
+
43
+ document.addEventListener('click', event => {
44
+ if (event.target.closest('reflex-behaviors-devools-tooltip')) return
45
+ this.hide()
46
+ })
47
+ }
48
+
49
+ get enabled () {
50
+ return supervisor.enabled(this.name)
51
+ }
52
+
53
+ show () {
54
+ if (!this.enabled) return
55
+ this.hide()
56
+ this.createTriggerTooltip()
57
+ this.createTargetTooltip()
58
+ addHighlight(this.target, { color: 'blue', offset: '-2px' })
59
+
60
+ let renderingPartial = this.trigger ? this.trigger.renderingPartial : null
61
+ renderingPartial =
62
+ renderingPartial || (this.target ? this.target.renderingPartial : null)
63
+
64
+ let renderingElement = this.trigger ? this.trigger.renderingElement : null
65
+ renderingElement =
66
+ renderingElement || (this.target ? this.target.renderingElement : null)
67
+
68
+ addHighlight(renderingElement, {
69
+ color: 'turquoise',
70
+ offset: '4px',
71
+ width: '4px'
72
+ })
73
+
74
+ const data = {
75
+ rendering: { partial: null, id: null },
76
+ trigger: { partial: null, id: null },
77
+ target: { partial: null, id: null }
78
+ }
79
+
80
+ if (renderingPartial) data.rendering.partial = renderingPartial
81
+ if (renderingElement) data.rendering.id = renderingElement.id
82
+
83
+ if (this.trigger)
84
+ data.trigger = { partial: this.trigger.partial, id: this.trigger.id }
85
+
86
+ if (this.target)
87
+ data.target = { partial: this.target.partial, id: this.target.id }
88
+ else if (this.trigger)
89
+ data.target.id = `No element matches the targeted DOM id: ${this.trigger.controls}`
90
+
91
+ console.table(data)
92
+ }
93
+
94
+ hide () {
95
+ let renderingElement = this.trigger ? this.trigger.renderingElement : null
96
+ renderingElement =
97
+ renderingElement || (this.target ? this.target.renderingElement : null)
98
+
99
+ this.destroyTriggerTooltip()
100
+ this.destroyTargetTooltip()
101
+ removeHighlight(this.target)
102
+ removeHighlight(renderingElement)
103
+ this.cleanup()
104
+ }
105
+
106
+ cleanup () {
107
+ document
108
+ .querySelectorAll('reflex-behaviors-devools-tooltip')
109
+ .forEach(el => el.remove())
110
+
111
+ document
112
+ .querySelectorAll('[data-reflex-behaviors-highlight]')
113
+ .forEach(el => {
114
+ if (!el.tagName.match(/toggle-trigger/i)) removeHighlight(el)
115
+ })
116
+ }
117
+
118
+ createTriggerTooltip () {
119
+ if (!this.trigger) return
120
+ const title = `TRIGGER (targets: ${this.trigger.controls})`
121
+ const content = this.trigger.viewStack
122
+ .map(view => {
123
+ return this.trigger.sharedViews.includes(view)
124
+ ? `<div slot="emphasis">${view}</div>`
125
+ : `<div slot="normal">${view}</div>`
126
+ }, this)
127
+ .join('')
128
+
129
+ this.triggerTooltip = appendTooltip(triggerTooltipId, title, content, {
130
+ backgroundColor: 'pink',
131
+ emphaisColor: 'darkred'
132
+ })
133
+
134
+ const coords = this.trigger.coordinates
135
+ const top = Math.ceil(coords.top - this.triggerTooltip.offsetHeight - 14)
136
+ const left = Math.ceil(coords.left - 15)
137
+ this.triggerTooltip.style.top = `${top}px`
138
+ this.triggerTooltip.style.left = `${left}px`
139
+ }
140
+
141
+ destroyTriggerTooltip () {
142
+ if (!this.triggerTooltip) return
143
+ this.triggerTooltip.remove()
144
+ delete this.triggerTooltip
145
+ }
146
+
147
+ createTargetTooltip () {
148
+ if (!this.target) return
149
+ if (!this.target.viewStack) return
150
+
151
+ const title = `TARGET (id: ${this.target.id})`
152
+ const content = this.target.viewStack
153
+ .map(view => {
154
+ return this.trigger.sharedViews.includes(view)
155
+ ? `<div slot="emphasis">${view}</div>`
156
+ : `<div slot="normal">${view}</div>`
157
+ }, this)
158
+ .join('')
159
+
160
+ this.targetTooltip = appendTooltip(targetTooltipId, title, content, {
161
+ backgroundColor: 'lightskyblue',
162
+ emphaisColor: 'blue',
163
+ position: 'bottom'
164
+ })
165
+
166
+ const coords = this.target.coordinates
167
+ const top = Math.ceil(coords.top + coords.height + 12)
168
+ const left = Math.ceil(coords.left - 15)
169
+ this.targetTooltip.style.top = `${top}px`
170
+ this.targetTooltip.style.left = `${left}px`
171
+ }
172
+
173
+ destroyTargetTooltip () {
174
+ if (!this.targetTooltip) return
175
+ this.targetTooltip.remove()
176
+ delete this.targetTooltip
177
+ }
178
+ }
@@ -1,6 +1,14 @@
1
1
  export default class ReflexElement extends HTMLElement {
2
+ constructor () {
3
+ super()
4
+ this.devtool = 'unknown'
5
+ this.attachShadow({ mode: 'open' })
6
+ this.shadowRoot.innerHTML = '<slot></slot>'
7
+ }
8
+
2
9
  connectedCallback () {
3
10
  this.ensureId()
11
+ this.dataset.elementOrigin = 'hopsoft/reflex_behaviors'
4
12
  }
5
13
 
6
14
  ensureId () {
@@ -23,6 +31,10 @@ export default class ReflexElement extends HTMLElement {
23
31
  return JSON.parse(this.dataset.viewStack)
24
32
  }
25
33
 
34
+ get partial () {
35
+ return this.viewStack[0]
36
+ }
37
+
26
38
  get coordinates () {
27
39
  const rect = this.getBoundingClientRect()
28
40
  return {
@@ -1,48 +1,23 @@
1
1
  import ReflexElement from './reflex_element'
2
- import devtools from '../devtools'
2
+ import DevtoolSupervisor from '../devtools/supervisor'
3
+ import ToggleDevtool from '../devtools/toggle'
3
4
 
4
5
  export default class ToggleTriggerElement extends ReflexElement {
5
- constructor () {
6
- super()
7
-
8
- this.addEventListener('mouseenter', event => {
9
- if (devtools.isEnabled('toggle')) {
10
- clearTimeout(this.mouseLeaveTimeout)
11
- event.target.target.classList.add('debug')
12
- event.target.showDebugTooltips()
13
- }
14
- })
6
+ connectedCallback () {
7
+ super.connectedCallback()
8
+ const mouseenter = () => this.devtool.show()
15
9
 
16
- this.addEventListener('mouseleave', event => {
17
- if (devtools.isEnabled('toggle')) {
18
- clearTimeout(this.mouseLeaveTimeout)
19
- this.mouseLeaveTimeout = setTimeout(() => {
20
- event.target.target.classList.remove('debug')
21
- document
22
- .querySelectorAll('.reflex-behaviors-tooltip')
23
- .forEach(tooltip => tooltip.remove())
24
- }, 250)
25
- }
10
+ document.addEventListener('reflex-behaviors:devtools-start', () => {
11
+ this.devtool = new ToggleDevtool(this)
12
+ this.addEventListener('mouseenter', mouseenter)
26
13
  })
27
- }
28
14
 
29
- showDebugTooltips () {
30
- const sharedViewPath = this.sharedViewPath
31
- let shared = false
32
- let title = `controls: ${this.controls}`
33
- let body = this.viewStack.map(path => {
34
- shared = shared || path === sharedViewPath
35
- return `<div class='${shared ? 'shared' : null}'>${path}<div>`
15
+ document.addEventListener('reflex-behaviors:devtools-stop', () => {
16
+ this.removeEventListener('mouseenter', mouseenter)
17
+ delete this.devtool
36
18
  })
37
- devtools.tooltip(this, title, body.join(''), 'trigger', 'top')
38
19
 
39
- shared = false
40
- title = `id: ${this.target.id}`
41
- body = this.target.viewStack.map(path => {
42
- shared = shared || path === sharedViewPath
43
- return `<div class='${shared ? 'shared' : null}'>${path}<div>`
44
- })
45
- devtools.tooltip(this.target, title, body.join(''), 'target', 'bottom')
20
+ if (DevtoolSupervisor.started) DevtoolSupervisor.restart()
46
21
  }
47
22
 
48
23
  collapse () {
@@ -54,19 +29,39 @@ export default class ToggleTriggerElement extends ReflexElement {
54
29
  }
55
30
  }
56
31
 
57
- get sharedViewPath () {
58
- const targetViewStack = this.target.viewStack
59
- return this.viewStack.find(path => targetViewStack.includes(path))
32
+ get sharedViews () {
33
+ if (!this.target) return []
34
+ if (!this.target.viewStack) return []
35
+ const reducer = (memo, view) => {
36
+ if (this.target.viewStack.includes(view)) memo.push(view)
37
+ return memo
38
+ }
39
+ return this.viewStack.reduce(reducer.bind(this), [])
60
40
  }
61
41
 
62
- get controls () {
63
- return this.getAttribute('aria-controls')
42
+ get renderingInfo () {
43
+ if (!this.dataset.render) return {}
44
+ return JSON.parse(this.dataset.render)
45
+ }
46
+
47
+ get renderingPartial () {
48
+ return this.renderingInfo.partial
49
+ }
50
+
51
+ get renderingElement () {
52
+ const { id } = this.renderingInfo
53
+ if (!id) return null
54
+ return document.getElementById(id)
64
55
  }
65
56
 
66
57
  get expanded () {
67
58
  return this.getAttribute('aria-expanded') === 'true'
68
59
  }
69
60
 
61
+ get controls () {
62
+ return this.getAttribute('aria-controls')
63
+ }
64
+
70
65
  get target () {
71
66
  return document.getElementById(this.controls)
72
67
  }
@@ -94,6 +89,7 @@ addEventListener(
94
89
  )
95
90
 
96
91
  addEventListener('click', event => {
92
+ if (event.target.tagName.match(/reflex-behaviors-devtool/i)) return
97
93
  setTimeout(() => {
98
94
  const selector =
99
95
  'toggle-trigger[aria-controls][aria-expanded="true"][data-auto-collapse="true"]'
@@ -5,8 +5,4 @@ import 'turbo_reflex'
5
5
  import './elements'
6
6
  import devtools from './devtools'
7
7
 
8
- const { enable, disable, start, stop } = devtools
9
-
10
- self.ReflexBehavors = {
11
- devtools: { enable, disable, start, stop }
12
- }
8
+ self.ReflexBehaviors = { devtools }
@@ -34,8 +34,11 @@ class ReflexBehaviors::ApplicationReflex < TurboReflex::Base
34
34
  private
35
35
 
36
36
  def hydrated_value(value)
37
- GlobalID::Locator.locate_signed(value)
38
- rescue
39
- value
37
+ hydrated = begin
38
+ GlobalID::Locator.locate_signed(value)
39
+ rescue
40
+ value
41
+ end
42
+ hydrated.blank? ? nil : hydrated
40
43
  end
41
44
  end
@@ -8,7 +8,6 @@ module ReflexBehaviors
8
8
  end
9
9
 
10
10
  class Engine < ::Rails::Engine
11
- config.assets.precompile << "reflex_behaviors_manifest.js"
12
11
  config.reflex_behaviors = ActiveSupport::OrderedOptions.new
13
12
 
14
13
  ActiveSupport.on_load(:action_controller) do
@@ -4,25 +4,6 @@ require_relative "base_tag_builder"
4
4
 
5
5
  module ReflexBehaviors::TagBuilders
6
6
  class ToggleTagsBuilder < BaseTagBuilder
7
- def target_tag(id, expanded: false, **kwargs, &block)
8
- kwargs = kwargs.with_indifferent_access
9
- kwargs[:id] = id
10
- kwargs[:role] = "region"
11
-
12
- kwargs[:aria] ||= {}
13
- kwargs[:aria][:label] ||= "Dynamic Content Region"
14
- kwargs[:aria][:live] ||= "polite"
15
-
16
- kwargs[:data] ||= {}
17
- kwargs[:data][:view_stack] = view_stack.to_json if Rails.env.development?
18
-
19
- if expanded || target_expanded?(id)
20
- content_tag("toggle-target", nil, kwargs, &block)
21
- else
22
- content_tag("toggle-target", nil, kwargs)
23
- end
24
- end
25
-
26
7
  def trigger_tag(target:, render:, action: :toggle, disabled: false, **kwargs, &block)
27
8
  kwargs = kwargs.with_indifferent_access
28
9
  kwargs[:id] ||= "#{target}-toggle-trigger"
@@ -42,6 +23,25 @@ module ReflexBehaviors::TagBuilders
42
23
  content_tag("toggle-trigger", nil, kwargs, &block)
43
24
  end
44
25
 
26
+ def target_tag(id, expanded: false, **kwargs, &block)
27
+ kwargs = kwargs.with_indifferent_access
28
+ kwargs[:id] = id
29
+ kwargs[:role] = "region"
30
+
31
+ kwargs[:aria] ||= {}
32
+ kwargs[:aria][:label] ||= "Dynamic Content Region"
33
+ kwargs[:aria][:live] ||= "polite"
34
+
35
+ kwargs[:data] ||= {}
36
+ kwargs[:data][:view_stack] = view_stack.to_json if Rails.env.development?
37
+
38
+ if expanded || target_expanded?(id)
39
+ content_tag("toggle-target", nil, kwargs, &block)
40
+ else
41
+ content_tag("toggle-target", nil, kwargs)
42
+ end
43
+ end
44
+
45
45
  def target_expanded?(target)
46
46
  !!turbo_reflex.state[target]
47
47
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReflexBehaviors
4
- VERSION = "0.0.3"
4
+ VERSION = "0.0.5"
5
5
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reflex_behaviors",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Pre-built easy to use reactive TurboReflex behaviors for Rails/Hotwire apps.",
5
5
  "main": "app/javascript/index.js",
6
6
  "repository": "https://github.com/hopsoft/reflex_behaviors",