reflex_behaviors 0.0.4 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  },