reflex_behaviors 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
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
+ return parent.appendChild(template(html).content.cloneNode(true))
10
+ }
11
+
12
+ export function addHighlight (element, options = {}) {
13
+ if (!element) return
14
+ let { color, offset, width } = options
15
+ color = color || 'red'
16
+ offset = offset || '0px'
17
+ width = width || '3px'
18
+ const { outline, outlineOffset } = element.style
19
+ element.originalStyles = element.originalStyles || {
20
+ outline,
21
+ outlineOffset
22
+ }
23
+ element.style.outline = `dotted ${width} ${color}`
24
+ element.style.outlineOffset = offset
25
+ }
26
+
27
+ export function removeHighlight (element) {
28
+ if (element && element.originalStyles) {
29
+ for (const [key, value] of Object.entries(element.originalStyles)) {
30
+ value ? (element.style[key] = value) : (element.style[key] = '')
31
+ }
32
+ delete element.originalStyles
33
+ }
34
+ }
@@ -0,0 +1,62 @@
1
+ export default class DevtoolElement extends HTMLElement {
2
+ constructor () {
3
+ super()
4
+ this.attachShadow({ mode: 'open' })
5
+ this.shadowRoot.innerHTML = this.html
6
+ this.labelElement.addEventListener('click', event => {
7
+ event.preventDefault()
8
+ this.toggle()
9
+ })
10
+ this.checkboxElement.addEventListener('change', event =>
11
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true }))
12
+ )
13
+ }
14
+
15
+ toggle () {
16
+ this.checked ? this.uncheck() : this.check()
17
+ }
18
+
19
+ check () {
20
+ this.checkboxElement.checked = true
21
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true }))
22
+ }
23
+
24
+ uncheck () {
25
+ this.checkboxElement.checked = false
26
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true }))
27
+ }
28
+
29
+ get name () {
30
+ return this.getAttribute('name')
31
+ }
32
+
33
+ get checked () {
34
+ return this.checkboxElement.checked
35
+ }
36
+
37
+ get checkboxElement () {
38
+ return this.shadowRoot.querySelector('input')
39
+ }
40
+
41
+ get labelElement () {
42
+ return this.shadowRoot.querySelector('label')
43
+ }
44
+
45
+ get html () {
46
+ return `
47
+ <style>${this.stylesheet}</style>
48
+ <div>
49
+ <input name="checkbox" type="checkbox">
50
+ <label for="checkbox"><slot name="label"></slot></label>
51
+ </div>
52
+ `
53
+ }
54
+
55
+ get stylesheet () {
56
+ return `
57
+ div {
58
+ display: flex;
59
+ }
60
+ `
61
+ }
62
+ }
@@ -0,0 +1,113 @@
1
+ import { appendHTML } from '../dom'
2
+
3
+ export default class SupervisorElement extends HTMLElement {
4
+ constructor () {
5
+ super()
6
+ this.enabledDevtools = {}
7
+ this.attachShadow({ mode: 'open' })
8
+ this.shadowRoot.innerHTML = this.html
9
+ this.shadowRoot
10
+ .querySelector('button[data-role="closer"]')
11
+ .addEventListener('click', () => this.close())
12
+
13
+ this.addEventListener('change', event => {
14
+ const devtoolElement = event.target
15
+ const { checked, name } = devtoolElement
16
+ checked ? this.enableDevtool(name) : this.disableDevtool(name)
17
+ })
18
+ }
19
+
20
+ enableDevtool (name) {
21
+ if (this.enabledDevtools[name]) return
22
+ this.enabledDevtools[name] = true
23
+ this.dispatchEvent(
24
+ new CustomEvent('reflex-behaviors:devtool-enable', {
25
+ bubbles: true,
26
+ detail: { name: name }
27
+ })
28
+ )
29
+ }
30
+
31
+ disableDevtool (name) {
32
+ if (!this.enabledDevtools[name]) return
33
+ delete this.enabledDevtools[name]
34
+ this.dispatchEvent(
35
+ new CustomEvent('reflex-behaviors:devtool-disable', {
36
+ bubbles: true,
37
+ detail: { name: name }
38
+ })
39
+ )
40
+ }
41
+
42
+ close () {
43
+ this.devtoolElements.forEach(el => {
44
+ if (el.checked) el.uncheck()
45
+ })
46
+ this.remove()
47
+ }
48
+
49
+ get devtoolElements () {
50
+ return this.querySelectorAll('[slot="devtool"]')
51
+ }
52
+
53
+ get closeElement () {
54
+ return this.querySelector('button[data-role="closer"]')
55
+ }
56
+
57
+ get html () {
58
+ return `
59
+ <style>${this.stylesheet}</style>
60
+ <div data-role="container">
61
+ <strong>ReflexBehaviors</strong>
62
+ <slot name="devtool"></slot>
63
+ <button data-role='closer'>X</button>
64
+ </div>
65
+ `
66
+ }
67
+
68
+ get stylesheet () {
69
+ return `
70
+ :host {
71
+ background-color: ghostwhite;
72
+ border-radius: 10px;
73
+ outline: solid 1px gainsboro;
74
+ bottom: 20px;
75
+ display: block;
76
+ filter: drop-shadow(0 4px 3px rgba(0,0,0,.07));
77
+ left: 50%;
78
+ padding: 5px 10px;
79
+ position: fixed;
80
+ transform: translateX(-50%);
81
+ z-index: 10000;
82
+ }
83
+
84
+ :host, :host * {
85
+ -webkit-user-select: none;
86
+ user-select: none;
87
+ }
88
+
89
+ strong {
90
+ color: silver;
91
+ font-weight: 600;
92
+ }
93
+
94
+ div[data-role="container"] {
95
+ display: flex;
96
+ gap: 0 5px;
97
+ }
98
+
99
+ button[data-role="closer"] {
100
+ border: none;
101
+ background-color: gainsboro;
102
+ border-radius: 50%;
103
+ color: white;
104
+ font-size: 12px;
105
+ height: 18px;
106
+ line-height: 18px;
107
+ margin: 0 -5px 0 10px;
108
+ padding: 0 3px;
109
+ width: 18px;
110
+ }
111
+ `
112
+ }
113
+ }
@@ -0,0 +1,102 @@
1
+ export default class TooltipElement extends HTMLElement {
2
+ constructor () {
3
+ super()
4
+ this.attachShadow({ mode: 'open' })
5
+ this.shadowRoot.innerHTML = this.html
6
+ }
7
+
8
+ get color () {
9
+ return this.getAttribute('color') || 'darkslategray'
10
+ }
11
+
12
+ get emphasisColor () {
13
+ return this.getAttribute('emphasis-color') || 'black'
14
+ }
15
+
16
+ get backgroundColor () {
17
+ return this.getAttribute('background-color') || 'gainsboro'
18
+ }
19
+
20
+ get position () {
21
+ return this.getAttribute('position') || 'top'
22
+ }
23
+
24
+ get cssArrow () {
25
+ switch (this.position) {
26
+ case 'bottom':
27
+ return `transparent transparent ${this.emphasisColor} transparent`
28
+ default:
29
+ return `${this.emphasisColor} transparent transparent transparent;`
30
+ }
31
+ }
32
+
33
+ get html () {
34
+ return `
35
+ <style>${this.stylesheet}</style>
36
+ <div role="tooltip">
37
+ <slot name="title"></slot>
38
+ <hr>
39
+ <slot name="normal"></slot>
40
+ <slot name="emphasis"></slot>
41
+ </div>
42
+ `
43
+ }
44
+
45
+ get stylesheet () {
46
+ return `
47
+ :host {
48
+ display: block;
49
+ position: absolute;
50
+ z-index: 10000;
51
+ }
52
+
53
+ * {
54
+ color: ${this.color}
55
+ }
56
+
57
+ [role="tooltip"] {
58
+ background-color: ${this.backgroundColor};
59
+ border-radius: 5px;
60
+ filter: drop-shadow(3px 3px 3px rgba(0,0,0,0.3));
61
+ font-family: monospace;
62
+ left: 50px;
63
+ min-height: 30px;
64
+ min-width: 100px;
65
+ opacity: 0.9;
66
+ outline: solid 2px ${this.emphasisColor};
67
+ padding: 8px 12px 8px 12px;
68
+ white-space: nowrap;
69
+ }
70
+
71
+ [role="tooltip"]::after {
72
+ border-color: ${this.cssArrow};
73
+ border-style: solid;
74
+ border-width: 5px;
75
+ content: "";
76
+ margin-left: -5px;
77
+ position: absolute;
78
+ top: ${this.position === 'bottom' ? '-10px' : '100%'};
79
+ }
80
+
81
+ slot[name="title"] {
82
+ color: ${this.emphasisColor};
83
+ font-weight: bold;
84
+ }
85
+
86
+ slot[name="emphasis"] {
87
+ color: ${this.emphasisColor};
88
+ font-weight: normal;
89
+ opacity: 0.7;
90
+ }
91
+
92
+ hr {
93
+ background-color: ${this.emphasisColor};
94
+ border: none;
95
+ height: 1px;
96
+ margin-bottom: 4px;
97
+ margin-top: 4px;
98
+ opacity: 0.3;
99
+ }
100
+ `
101
+ }
102
+ }
@@ -1,117 +1,5 @@
1
- // Tasks
2
- // - [ ] audit css class names and refine
3
- // - [ ] extract stylesheet and host on cdn
4
- // - [ ] ensure tooltips don't overlap or run off screen
5
- // - [ ] add ability to remember start/stop in local storage
6
- // - [ ] isolate individual tools and register them (i.e. plugin framework)
7
- // will probably have behaviors register any devtools they support
8
- //
9
- // Tool markup example
10
- //
11
- // <div name="TOOL_NAME" class="devtool">
12
- // <input name="TOOL_NAME-checkbox" value="TOOL_NAME" type="checkbox">
13
- // <label for="TOOL_NAME-checkbox">TOOL_LABEL</label>
14
- // </div>
1
+ import supervisor from './supervisor'
15
2
 
16
- let tray
3
+ const { restart, start, stop } = supervisor
17
4
 
18
- addEventListener('click', () => {
19
- setTimeout(() => {
20
- document
21
- .querySelectorAll('.reflex-behaviors-tooltip')
22
- .forEach(tooltip => tooltip.remove())
23
- }, 300)
24
- })
25
-
26
- function tooltip (reflexElement, title, body, cssClass, position = 'top') {
27
- const el = document.createElement('div')
28
- el.classList.add('reflex-behaviors-tooltip', cssClass)
29
- el.innerHTML = `<strong>${title}</strong><hr>${body}`
30
- document.body.appendChild(el)
31
-
32
- const coords = reflexElement.coordinates
33
-
34
- if (position === 'top') {
35
- el.style.top = `${Math.ceil(coords.top - el.offsetHeight - 5)}px`
36
- el.style.left = `${Math.ceil(coords.left + 4)}px`
37
- }
38
-
39
- if (position === 'bottom') {
40
- el.style.top = `${Math.ceil(coords.top + coords.height + 5)}px`
41
- el.style.left = `${Math.ceil(coords.left + 4)}px`
42
- }
43
- }
44
-
45
- function enabled () {
46
- const tools = document.body.dataset.devtools || ''
47
- return tools.trim().split(' ')
48
- }
49
-
50
- function isEnabled (tool) {
51
- return enabled().includes(tool)
52
- }
53
-
54
- function enable (tool) {
55
- if (isEnabled(tool)) return
56
- const list = enabled()
57
- list.push(tool)
58
- document.body.dataset.devtools = list.join(' ').trim()
59
- }
60
-
61
- function disable (tool) {
62
- const list = enabled()
63
- const index = list.indexOf(tool)
64
- if (index < 0) return
65
- list.splice(index, 1)
66
- document.body.dataset.devtools = list.join(' ').trim()
67
- }
68
-
69
- function stop () {
70
- if (!tray) return
71
- tray
72
- .querySelectorAll('.devtool')
73
- .forEach(devtool => disable(devtool.getAttribute('name')))
74
- tray.remove()
75
- tray = null
76
- }
77
-
78
- function start () {
79
- stop()
80
- tray = document.createElement('div')
81
- tray.id = 'reflex-behaviors-devtools'
82
- tray.innerHTML = `
83
- <strong>DEVTOOLS</strong>
84
- <div name="toggle" class="devtool">
85
- <input name="toggle-checkbox" value="toggle" type="checkbox">
86
- <label for="toggle-checkbox">toggles<small>(trigger/target)</small></label>
87
- </div>
88
- <button data-action='close'>X</button>
89
- `
90
- document.body.appendChild(tray)
91
- tray
92
- .querySelector('button[data-action=close]')
93
- .addEventListener('click', () => stop())
94
- tray.querySelectorAll('.devtool').forEach(devtool => {
95
- devtool.querySelector('input').addEventListener('change', event => {
96
- event.target.checked
97
- ? enable(event.target.value)
98
- : disable(event.target.value)
99
- })
100
- devtool.addEventListener('click', event => {
101
- if (event.target.tagName.match(/input/i)) return
102
- event.target
103
- .closest('.devtool')
104
- .querySelector('input')
105
- .click()
106
- })
107
- })
108
- }
109
-
110
- export default {
111
- disable,
112
- enable,
113
- isEnabled,
114
- start,
115
- stop,
116
- tooltip
117
- }
5
+ export default { restart, start, stop }
@@ -0,0 +1,83 @@
1
+ import { appendHTML } from './dom'
2
+ import DevtoolElement from './elements/devtool_element'
3
+ import SupervisorElement from './elements/supervisor_element'
4
+ import TooltipElement from './elements/tooltip_element'
5
+
6
+ customElements.define('reflex-behaviors-devtool', DevtoolElement)
7
+ customElements.define('reflex-behaviors-devtool-supervisor', SupervisorElement)
8
+ customElements.define('reflex-behaviors-devools-tooltip', TooltipElement)
9
+
10
+ let supervisorElement
11
+
12
+ function stop () {
13
+ if (!supervisorElement) return
14
+ supervisorElement.close()
15
+ supervisorElement.dispatchEvent(
16
+ new CustomEvent('reflex-behaviors:devtools-stop', {
17
+ bubbles: true
18
+ })
19
+ )
20
+ supervisorElement = null
21
+ }
22
+
23
+ function start () {
24
+ appendHTML(
25
+ '<reflex-behaviors-devtool-supervisor></reflex-behaviors-devtool-supervisor>'
26
+ )
27
+ supervisorElement = document.body.querySelector(
28
+ 'reflex-behaviors-devtool-supervisor'
29
+ )
30
+ supervisorElement.dispatchEvent(
31
+ new CustomEvent('reflex-behaviors:devtools-start', {
32
+ bubbles: true
33
+ })
34
+ )
35
+ }
36
+
37
+ function restart () {
38
+ const enabledList = supervisorElement
39
+ ? Object.keys(supervisorElement.enabledDevtools)
40
+ : []
41
+
42
+ stop()
43
+ start()
44
+
45
+ supervisorElement.devtoolElements.forEach(el => {
46
+ if (enabledList.includes(el.name)) el.check()
47
+ })
48
+ }
49
+
50
+ let restartTimeout
51
+ function debouncedRestart () {
52
+ clearTimeout(restartTimeout)
53
+ restartTimeout = setTimeout(restart, 25)
54
+ }
55
+ addEventListener('turbo:load', debouncedRestart)
56
+ addEventListener('turbo-frame:load', debouncedRestart)
57
+ addEventListener('turbo-reflex:success', debouncedRestart)
58
+ addEventListener('turbo-reflex:finish', debouncedRestart)
59
+
60
+ function register (name, label) {
61
+ if (!supervisorElement) return
62
+ return appendHTML(
63
+ `
64
+ <reflex-behaviors-devtool name="${name}" slot="devtool">
65
+ <span slot="label">${label}</span>
66
+ </reflex-behaviors-devtool>
67
+ `,
68
+ supervisorElement
69
+ )
70
+ }
71
+
72
+ function enabled (name) {
73
+ if (!supervisorElement) return false
74
+ return supervisorElement.enabledDevtools[name]
75
+ }
76
+
77
+ export default {
78
+ enabled,
79
+ register,
80
+ restart: debouncedRestart,
81
+ start,
82
+ stop
83
+ }
@@ -0,0 +1,150 @@
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
+ 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
23
+ color = color || 'white'
24
+ position = position || 'top'
25
+
26
+ appendHTML(`
27
+ <reflex-behaviors-devools-tooltip id="${id}" position="${position}" background-color="${backgroundColor}" color="${color}" emphasis-color="${emphaisColor}">
28
+ <div slot='title'>${title}</div>
29
+ ${content}
30
+ </reflex-behaviors-devools-tooltip>
31
+ `)
32
+ return document.getElementById(id)
33
+ }
34
+
35
+ export default class ToggleDevtool {
36
+ constructor (trigger) {
37
+ this.name = 'toggle'
38
+ this.reflex = trigger.dataset.turboReflex
39
+ this.trigger = trigger
40
+ 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
+
47
+ document.addEventListener('reflex-behaviors:devtool-enable', event => {
48
+ const { name } = event.detail
49
+ if (name === this.name)
50
+ addHighlight(this.trigger, { color: 'red', offset: '2px' })
51
+ })
52
+
53
+ document.addEventListener('reflex-behaviors:devtool-disable', event => {
54
+ const { name } = event.detail
55
+ if (name === this.name) removeHighlight(this.trigger)
56
+ })
57
+ }
58
+
59
+ get enabled () {
60
+ return supervisor.enabled(this.name)
61
+ }
62
+
63
+ show () {
64
+ if (!this.enabled) return
65
+ 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'
73
+ })
74
+
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
+ }
82
+ })
83
+ }
84
+
85
+ hide () {
86
+ this.destroyTriggerTooltip()
87
+ this.destroyTargetTooltip()
88
+ removeHighlight(this.target)
89
+ removeHighlight(this.renderingElement)
90
+ }
91
+
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('')
101
+
102
+ this.triggerTooltip = appendTooltip(triggerTooltipId, title, content, {
103
+ backgroundColor: 'pink',
104
+ emphaisColor: 'darkred'
105
+ })
106
+
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
+ }
113
+
114
+ destroyTriggerTooltip () {
115
+ if (!this.triggerTooltip) return
116
+ this.triggerTooltip.remove()
117
+ delete this.triggerTooltip
118
+ }
119
+
120
+ createTargetTooltip () {
121
+ if (!this.target) return
122
+
123
+ const title = `TARGET (id: ${this.target.id})`
124
+ const content = this.target.viewStack
125
+ .map(view => {
126
+ return this.trigger.sharedViews.includes(view)
127
+ ? `<div slot="emphasis">${view}</div>`
128
+ : `<div slot="normal">${view}</div>`
129
+ }, this)
130
+ .join('')
131
+
132
+ this.targetTooltip = appendTooltip(targetTooltipId, title, content, {
133
+ backgroundColor: 'lightskyblue',
134
+ emphaisColor: 'blue',
135
+ position: 'bottom'
136
+ })
137
+
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`
143
+ }
144
+
145
+ destroyTargetTooltip () {
146
+ if (!this.targetTooltip) return
147
+ this.targetTooltip.remove()
148
+ delete this.targetTooltip
149
+ }
150
+ }
@@ -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 {