reflex_behaviors 0.0.4 → 0.0.6

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.
@@ -1,35 +1,27 @@
1
- import { appendHTML, addHighlight, removeHighlight } from './dom'
1
+ import {
2
+ appendHTML,
3
+ addHighlight,
4
+ coordinates,
5
+ removeHighlight
6
+ } from '../utils/dom'
2
7
  import supervisor from './supervisor'
3
8
 
9
+ let activeToggle
10
+
4
11
  document.addEventListener('reflex-behaviors:devtools-start', () =>
5
12
  supervisor.register('toggle', 'toggles<small>(trigger/target)</small>')
6
13
  )
7
14
 
8
- const triggerTooltipId = 'toggle-trigger-tooltip'
9
- const targetTooltipId = 'toggle-target-tooltip'
10
-
11
- addEventListener('click', () => setTimeout(removeTooltips))
12
-
13
- function removeTooltips () {
14
- const ids = [triggerTooltipId, targetTooltipId]
15
- ids.forEach(id => {
16
- const el = document.getElementById(id)
17
- if (el) el.remove()
18
- })
19
- }
20
-
21
- function appendTooltip (id, title, content, options = {}) {
22
- let { backgroundColor, color, emphaisColor, position } = options
15
+ function appendTooltip (title, content, options = {}) {
16
+ let { backgroundColor, color, position } = options
23
17
  color = color || 'white'
24
18
  position = position || 'top'
25
-
26
- appendHTML(`
27
- <reflex-behaviors-devools-tooltip id="${id}" position="${position}" background-color="${backgroundColor}" color="${color}" emphasis-color="${emphaisColor}">
19
+ return appendHTML(`
20
+ <reflex-behaviors-devools-tooltip position="${position}" background-color="${backgroundColor}" color="${color}">
28
21
  <div slot='title'>${title}</div>
29
22
  ${content}
30
23
  </reflex-behaviors-devools-tooltip>
31
24
  `)
32
- return document.getElementById(id)
33
25
  }
34
26
 
35
27
  export default class ToggleDevtool {
@@ -38,22 +30,27 @@ export default class ToggleDevtool {
38
30
  this.reflex = trigger.dataset.turboReflex
39
31
  this.trigger = trigger
40
32
  this.target = trigger.target
41
- this.renderingPartial = trigger.renderingPartial
42
- this.renderingElement = trigger.renderingElement
43
- this.renderingElementId = this.renderingElement
44
- ? this.renderingElement.id
45
- : null
46
33
 
47
34
  document.addEventListener('reflex-behaviors:devtool-enable', event => {
48
35
  const { name } = event.detail
49
- if (name === this.name)
50
- addHighlight(this.trigger, { color: 'red', offset: '2px' })
36
+ if (name === this.name) {
37
+ addHighlight(this.trigger, {
38
+ outline: '3px dashed blueviolet',
39
+ outlineOffset: '2px'
40
+ })
41
+ }
51
42
  })
52
43
 
53
44
  document.addEventListener('reflex-behaviors:devtool-disable', event => {
54
45
  const { name } = event.detail
55
46
  if (name === this.name) removeHighlight(this.trigger)
56
47
  })
48
+
49
+ document.addEventListener('click', event => {
50
+ if (event.target.closest('reflex-behaviors-devools-tooltip')) return
51
+ activeToggle = null
52
+ this.hide()
53
+ })
57
54
  }
58
55
 
59
56
  get enabled () {
@@ -62,89 +59,204 @@ export default class ToggleDevtool {
62
59
 
63
60
  show () {
64
61
  if (!this.enabled) return
62
+ if (activeToggle === this) return
63
+ activeToggle = this
65
64
  this.hide()
66
- this.createTriggerTooltip()
67
- this.createTargetTooltip()
68
- addHighlight(this.target, { color: 'blue', offset: '-2px' })
69
- addHighlight(this.renderingElement, {
70
- color: 'turquoise',
71
- offset: '4px',
72
- width: '4px'
65
+
66
+ addHighlight(this.target, {
67
+ outline: '3px dashed darkcyan',
68
+ outlineOffset: '-2px'
73
69
  })
74
70
 
75
- console.table({
76
- trigger: { id: this.trigger.id, partial: this.trigger.partial },
77
- target: { id: this.target.id, partial: this.target.partial },
78
- [this.reflex]: {
79
- id: this.renderingElementId,
80
- partial: this.renderingPartial
81
- }
71
+ addHighlight(this.renderingElement, {
72
+ outline: '3px dashed chocolate',
73
+ outlineOffset: '3px'
82
74
  })
75
+
76
+ const renderingTooltip = this.createRenderingTooltip()
77
+ const targetTooltip = this.createTargetTooltip()
78
+ this.createTriggerTooltip(targetTooltip, renderingTooltip)
79
+
80
+ document
81
+ .querySelectorAll('.leader-line')
82
+ .forEach(el => (el.style.zIndex = 100000))
83
+
84
+ const data = {
85
+ rendering: { partial: null, id: null },
86
+ trigger: { partial: null, id: null },
87
+ target: { partial: null, id: null }
88
+ }
89
+
90
+ if (this.renderingPartial) data.rendering.partial = this.renderingPartial
91
+ if (this.renderingElement) data.rendering.id = this.renderingElement.id
92
+
93
+ if (this.trigger)
94
+ data.trigger = { partial: this.trigger.partial, id: this.trigger.id }
95
+
96
+ if (this.target)
97
+ data.target = { partial: this.target.partial, id: this.target.id }
98
+ else if (this.trigger)
99
+ data.target.id = `No element matches the targeted DOM id: ${this.trigger.controls}`
100
+
101
+ console.table(data)
83
102
  }
84
103
 
85
104
  hide () {
86
- this.destroyTriggerTooltip()
87
- this.destroyTargetTooltip()
88
- removeHighlight(this.target)
89
- removeHighlight(this.renderingElement)
90
- }
105
+ document.querySelectorAll('.leader-line').forEach(el => el.remove())
106
+ document
107
+ .querySelectorAll('reflex-behaviors-devools-tooltip')
108
+ .forEach(el => el.remove())
91
109
 
92
- createTriggerTooltip () {
93
- const title = `TRIGGER (targets: ${this.trigger.controls})`
94
- const content = this.trigger.viewStack
95
- .map(view => {
96
- return this.trigger.sharedViews.includes(view)
97
- ? `<div slot="emphasis">${view}</div>`
98
- : `<div slot="normal">${view}</div>`
99
- }, this)
100
- .join('')
110
+ document
111
+ .querySelectorAll('[data-reflex-behaviors-highlight]')
112
+ .forEach(el => {
113
+ if (!el.tagName.match(/toggle-trigger/i)) removeHighlight(el)
114
+ })
115
+ }
101
116
 
102
- this.triggerTooltip = appendTooltip(triggerTooltipId, title, content, {
103
- backgroundColor: 'pink',
104
- emphaisColor: 'darkred'
117
+ createRenderingTooltip () {
118
+ if (!this.renderingElement) return
119
+ const title = `RENDERING (id: ${this.renderingElement.id || 'unknown'})`
120
+ const content = `<div slot="content">partial: ${this.renderingPartial}</div>`
121
+ const tooltip = appendTooltip(title, content, {
122
+ backgroundColor: 'lightyellow',
123
+ color: 'chocolate'
105
124
  })
106
125
 
107
- const coords = this.trigger.coordinates
108
- const top = Math.ceil(coords.top - this.triggerTooltip.offsetHeight - 5)
109
- const left = Math.ceil(coords.left)
110
- this.triggerTooltip.style.top = `${top}px`
111
- this.triggerTooltip.style.left = `${left}px`
112
- }
126
+ const coords = coordinates(this.renderingElement)
127
+ const top = Math.ceil(
128
+ coords.top + coords.height / 2 - tooltip.offsetHeight / 2
129
+ )
130
+ const left = Math.ceil(coords.left + coords.width + 100)
131
+ tooltip.style.top = `${top}px`
132
+ tooltip.style.left = `${left}px`
133
+
134
+ tooltip.line = new LeaderLine(tooltip, this.renderingElement, {
135
+ ...this.leaderLineOptions,
136
+ color: 'chocolate'
137
+ })
113
138
 
114
- destroyTriggerTooltip () {
115
- if (!this.triggerTooltip) return
116
- this.triggerTooltip.remove()
117
- delete this.triggerTooltip
139
+ tooltip.drag = new PlainDraggable(tooltip)
140
+ return tooltip
118
141
  }
119
142
 
120
143
  createTargetTooltip () {
121
144
  if (!this.target) return
145
+ if (!this.target.viewStack) return
122
146
 
123
147
  const title = `TARGET (id: ${this.target.id})`
124
148
  const content = this.target.viewStack
125
- .map(view => {
149
+ .reverse()
150
+ .map((view, index) => {
126
151
  return this.trigger.sharedViews.includes(view)
127
- ? `<div slot="emphasis">${view}</div>`
128
- : `<div slot="normal">${view}</div>`
152
+ ? `<div slot="content-top">${index + 1}. ${view}</div>`
153
+ : `<div slot="content-bottom">${index + 1}. ${view}</div>`
129
154
  }, this)
130
155
  .join('')
131
156
 
132
- this.targetTooltip = appendTooltip(targetTooltipId, title, content, {
133
- backgroundColor: 'lightskyblue',
134
- emphaisColor: 'blue',
157
+ const tooltip = appendTooltip(title, content, {
158
+ backgroundColor: 'lightcyan',
159
+ color: 'darkcyan',
135
160
  position: 'bottom'
136
161
  })
137
162
 
138
- const coords = this.target.coordinates
139
- const top = Math.ceil(coords.top + coords.height + 4)
140
- const left = Math.ceil(coords.left)
141
- this.targetTooltip.style.top = `${top}px`
142
- this.targetTooltip.style.left = `${left}px`
163
+ const coords = coordinates(this.target)
164
+ const top = Math.ceil(coords.top + tooltip.offsetHeight)
165
+ const left = Math.ceil(coords.left + coords.width + tooltip.offsetWidth / 3)
166
+ tooltip.style.top = `${top}px`
167
+ tooltip.style.left = `${left}px`
168
+
169
+ tooltip.line = new LeaderLine(tooltip, this.target, {
170
+ ...this.leaderLineOptions,
171
+ color: 'darkcyan'
172
+ })
173
+
174
+ tooltip.drag = new PlainDraggable(tooltip)
175
+ return tooltip
176
+ }
177
+
178
+ createTriggerTooltip (targetTooltip, renderingTooltip) {
179
+ if (!this.trigger) return
180
+ const title = `TRIGGER (controls: ${this.trigger.controls})`
181
+ const content = this.trigger.viewStack
182
+ .reverse()
183
+ .map((view, index) => {
184
+ return this.trigger.sharedViews.includes(view)
185
+ ? `<div slot="content-top">${index + 1}. ${view}</div>`
186
+ : `<div slot="content-bottom">${index + 1}. ${view}</div>`
187
+ }, this)
188
+ .join('')
189
+
190
+ const tooltip = appendTooltip(title, content, {
191
+ backgroundColor: 'lavender',
192
+ color: 'blueviolet'
193
+ })
194
+
195
+ const coords = coordinates(this.trigger)
196
+ const top = Math.ceil(coords.top - tooltip.offsetHeight * 2)
197
+ const left = Math.ceil(coords.left + coords.width + tooltip.offsetWidth / 3)
198
+ tooltip.style.top = `${top}px`
199
+ tooltip.style.left = `${left}px`
200
+
201
+ tooltip.line = new LeaderLine(this.trigger, tooltip, {
202
+ ...this.leaderLineOptions,
203
+ color: 'blueviolet'
204
+ })
205
+
206
+ tooltip.lineToTarget = new LeaderLine(tooltip, targetTooltip, {
207
+ ...this.leaderLineOptions,
208
+ color: 'blueviolet',
209
+ middleLabel: 'toggles',
210
+ size: 2.1
211
+ })
212
+
213
+ tooltip.lineToRendering = new LeaderLine(tooltip, renderingTooltip, {
214
+ ...this.leaderLineOptions,
215
+ color: 'blueviolet',
216
+ middleLabel: 'renders',
217
+ size: 2.1
218
+ })
219
+
220
+ tooltip.drag = new PlainDraggable(tooltip)
221
+ tooltip.drag.onMove = () => {
222
+ tooltip.line.position()
223
+ tooltip.lineToTarget.position()
224
+ tooltip.lineToRendering.position()
225
+ }
226
+ targetTooltip.drag.onMove = () => {
227
+ targetTooltip.line.position()
228
+ tooltip.lineToTarget.position()
229
+ tooltip.lineToRendering.position()
230
+ }
231
+ renderingTooltip.drag.onMove = () => {
232
+ renderingTooltip.line.position()
233
+ tooltip.lineToTarget.position()
234
+ tooltip.lineToRendering.position()
235
+ }
236
+ return tooltip
237
+ }
238
+
239
+ get renderingPartial () {
240
+ let partial = this.trigger ? this.trigger.renderingPartial : null
241
+ partial = partial || (this.target ? this.target.renderingPartial : null)
242
+ return partial
243
+ }
244
+
245
+ get renderingElement () {
246
+ let element = this.trigger ? this.trigger.renderingElement : null
247
+ element = element || (this.target ? this.target.renderingElement : null)
248
+ return element
143
249
  }
144
250
 
145
- destroyTargetTooltip () {
146
- if (!this.targetTooltip) return
147
- this.targetTooltip.remove()
148
- delete this.targetTooltip
251
+ get leaderLineOptions () {
252
+ return {
253
+ dash: { animation: true },
254
+ dropShadow: { opacity: 0.3 },
255
+ endPlug: 'arrow3',
256
+ endPlugSize: 1.7,
257
+ size: 3,
258
+ startPlug: 'disc',
259
+ startPlugSize: 1
260
+ }
149
261
  }
150
262
  }
@@ -34,14 +34,4 @@ export default class ReflexElement extends HTMLElement {
34
34
  get partial () {
35
35
  return this.viewStack[0]
36
36
  }
37
-
38
- get coordinates () {
39
- const rect = this.getBoundingClientRect()
40
- return {
41
- left: rect.left + window.scrollX,
42
- top: rect.top + window.scrollY,
43
- width: this.offsetWidth,
44
- height: this.offsetHeight
45
- }
46
- }
47
37
  }
@@ -5,23 +5,19 @@ import ToggleDevtool from '../devtools/toggle'
5
5
  export default class ToggleTriggerElement extends ReflexElement {
6
6
  connectedCallback () {
7
7
  super.connectedCallback()
8
-
9
8
  const mouseenter = () => this.devtool.show()
10
- const mouseleave = () => this.devtool.hide()
11
9
 
12
10
  document.addEventListener('reflex-behaviors:devtools-start', () => {
13
11
  this.devtool = new ToggleDevtool(this)
14
12
  this.addEventListener('mouseenter', mouseenter)
15
- this.addEventListener('mouseleave', mouseleave)
16
13
  })
17
14
 
18
15
  document.addEventListener('reflex-behaviors:devtools-stop', () => {
19
16
  this.removeEventListener('mouseenter', mouseenter)
20
- this.removeEventListener('mouseleave', mouseleave)
21
17
  delete this.devtool
22
18
  })
23
19
 
24
- DevtoolSupervisor.restart()
20
+ if (DevtoolSupervisor.started) DevtoolSupervisor.restart()
25
21
  }
26
22
 
27
23
  collapse () {
@@ -35,6 +31,7 @@ export default class ToggleTriggerElement extends ReflexElement {
35
31
 
36
32
  get sharedViews () {
37
33
  if (!this.target) return []
34
+ if (!this.target.viewStack) return []
38
35
  const reducer = (memo, view) => {
39
36
  if (this.target.viewStack.includes(view)) memo.push(view)
40
37
  return memo
@@ -0,0 +1,62 @@
1
+ export function template (html) {
2
+ let template = document.createElement('template')
3
+ template.innerHTML = html
4
+ return template
5
+ }
6
+
7
+ export function appendHTML (html, parent) {
8
+ parent = parent || document.body
9
+ const clone = template(html).content.cloneNode(true)
10
+ const child = clone.querySelector('*')
11
+ return parent.appendChild(child)
12
+ }
13
+
14
+ export function addHighlight (element, options = {}) {
15
+ if (!element) return
16
+ removeHighlight(element)
17
+ let { outline, outlineOffset } = options
18
+
19
+ outline = outline || 'dashed 3px red'
20
+ outlineOffset = outlineOffset || '0px'
21
+
22
+ element.originalStyles = element.originalStyles || {
23
+ display: element.style.display,
24
+ minHeight: element.style.minHeight,
25
+ minWidth: element.style.minWidth,
26
+ outline: element.style.outline,
27
+ outlineOffset: element.style.outlineOffset
28
+ }
29
+
30
+ if (
31
+ getComputedStyle(element).display.match(/^inline$/i) &&
32
+ element.offsetWidth === 0 &&
33
+ element.offsetHeight === 0
34
+ ) {
35
+ element.style.display = 'inline-block'
36
+ element.style.minHeight = '2px'
37
+ element.style.minWidth = '2px'
38
+ }
39
+ element.style.outline = outline
40
+ element.style.outlineOffset = outlineOffset
41
+ element.dataset.reflexBehaviorsHighlight = true
42
+ }
43
+
44
+ export function removeHighlight (element) {
45
+ if (!element) return
46
+ if (element.originalStyles) {
47
+ for (const [key, value] of Object.entries(element.originalStyles))
48
+ value ? (element.style[key] = value) : (element.style[key] = '')
49
+ delete element.originalStyles
50
+ }
51
+ delete element.dataset.reflexBehaviorsHighlight
52
+ }
53
+
54
+ export function coordinates (element) {
55
+ const rect = element.getBoundingClientRect()
56
+ return {
57
+ left: rect.left + window.scrollX,
58
+ top: rect.top + window.scrollY,
59
+ width: element.offsetWidth,
60
+ height: element.offsetHeight
61
+ }
62
+ }
@@ -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
@@ -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.4"
4
+ VERSION = "0.0.6"
5
5
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reflex_behaviors",
3
- "version": "0.0.3",
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",
@@ -15,7 +15,7 @@
15
15
  "@hotwired/turbo-rails": ">= 7.2"
16
16
  },
17
17
  "devDependencies": {
18
- "esbuild": "^0.15.7",
18
+ "esbuild": "^0.16.1",
19
19
  "eslint": "^8.19.0",
20
20
  "prettier-standard": "^16.4.1"
21
21
  },