simplyflow 0.8.2 → 0.9.0
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.
- package/dist/simply.flow.js +207 -100
- package/dist/simply.flow.min.js +1 -1
- package/dist/simply.flow.min.js.map +4 -4
- package/package.json +1 -1
- package/src/bind.mjs +3 -5
- package/src/bind.render.mjs +18 -10
- package/src/dom.mjs +39 -5
- package/src/edit/anchor.mjs +130 -0
- package/src/edit/toolbars.mjs +315 -0
- package/src/edit.mjs +168 -83
- package/src/model.mjs +2 -1
- package/src/state.mjs +205 -95
- package/src/symbols.mjs +8 -0
package/src/bind.mjs
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { throttledEffect, destroy } from './state.mjs'
|
|
2
2
|
import { escape_html, fixed_content } from './bind.transformers.mjs'
|
|
3
3
|
import * as render from './bind.render.mjs'
|
|
4
|
-
|
|
5
|
-
if (!Symbol.bindTemplate) {
|
|
6
|
-
Symbol.bindTemplate = Symbol('bindTemplate')
|
|
7
|
-
}
|
|
4
|
+
import { DEP } from './symbols.mjs'
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* Implements one way databinding, updating dom elements with matching attributes
|
|
@@ -48,6 +45,7 @@ class SimplyBind
|
|
|
48
45
|
},
|
|
49
46
|
renderers: {
|
|
50
47
|
'INPUT':render.input,
|
|
48
|
+
'TEXTAREA':render.input,
|
|
51
49
|
'BUTTON':render.button,
|
|
52
50
|
'SELECT':render.select,
|
|
53
51
|
'A':render.anchor,
|
|
@@ -255,7 +253,7 @@ class SimplyBind
|
|
|
255
253
|
clone.children[0].setAttribute(attribute+'-key',index)
|
|
256
254
|
}
|
|
257
255
|
// keep track of the used template, so if that changes, the item can be updated
|
|
258
|
-
clone.children[0][
|
|
256
|
+
clone.children[0][DEP.TEMPLATE] = template
|
|
259
257
|
|
|
260
258
|
// return clone, not the firstChild, so that all whitespace is cloned as well
|
|
261
259
|
return clone
|
package/src/bind.render.mjs
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { signal as domSignal, trackDomField, trackDomList } from './dom.mjs'
|
|
6
6
|
import { throttledEffect, effect, untracked, batch } from './state.mjs'
|
|
7
7
|
import { getValueByPath } from './bind.mjs'
|
|
8
|
+
import { DEP } from './symbols.mjs'
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* This function is used by default to render dom elements with the `data-flow-field` attribute.
|
|
10
12
|
* It will switch to only switching in template content if the context has any templates.
|
|
@@ -147,7 +149,8 @@ export function setValueByPath(root, path, value)
|
|
|
147
149
|
export function arrayByTemplates(context)
|
|
148
150
|
{
|
|
149
151
|
const attribute = this.options.attribute
|
|
150
|
-
|
|
152
|
+
const attributes = [attribute+'-field',attribute+'-list',attribute+'-map']
|
|
153
|
+
const attrQuery = '['+attributes.join('],[')+']'
|
|
151
154
|
let items = context.element.querySelectorAll(':scope > ['+attribute+'-key]')
|
|
152
155
|
// do single merge strategy for now, in future calculate optimal merge strategy from a number
|
|
153
156
|
// now just do a delete if a key <= last key, insert if a key >= last key
|
|
@@ -165,19 +168,24 @@ export function arrayByTemplates(context)
|
|
|
165
168
|
item.remove()
|
|
166
169
|
} else {
|
|
167
170
|
// check that all data-bind params start with current json path or ':root', otherwise replaceChild
|
|
168
|
-
let bindings = Array.from(item.querySelectorAll(
|
|
169
|
-
if (item.matches(
|
|
171
|
+
let bindings = Array.from(item.querySelectorAll(attrQuery))
|
|
172
|
+
if (item.matches(attrQuery)) {
|
|
170
173
|
bindings.unshift(item)
|
|
171
174
|
}
|
|
172
175
|
let needsReplacement = bindings.find(b => {
|
|
173
|
-
let
|
|
174
|
-
|
|
175
|
-
&& databind.substr(0,
|
|
176
|
+
for (let attr of attributes) {
|
|
177
|
+
let databind = b.getAttribute(attr)
|
|
178
|
+
if (databind && databind.substr(0,5)!==':root'
|
|
179
|
+
&& databind.substr(0, context.path.length)!==context.path) {
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return false
|
|
176
184
|
})
|
|
177
185
|
if (!needsReplacement) {
|
|
178
|
-
if (item[
|
|
186
|
+
if (item[DEP.TEMPLATE]) {
|
|
179
187
|
let newTemplate = this.findTemplate(context.templates, context.list[lastKey])
|
|
180
|
-
if (newTemplate != item[
|
|
188
|
+
if (newTemplate != item[DEP.TEMPLATE]){
|
|
181
189
|
needsReplacement = true
|
|
182
190
|
if (!newTemplate) {
|
|
183
191
|
skipped++
|
|
@@ -249,7 +257,7 @@ export function objectByTemplates(context)
|
|
|
249
257
|
}
|
|
250
258
|
}
|
|
251
259
|
let newTemplate = this.findTemplate(context.templates, context.list[context.index])
|
|
252
|
-
if (newTemplate != item[
|
|
260
|
+
if (newTemplate != item[DEP.TEMPLATE]){
|
|
253
261
|
let clone = this.applyTemplate(context)
|
|
254
262
|
context.element.replaceChild(clone, item)
|
|
255
263
|
}
|
|
@@ -272,7 +280,7 @@ export function fieldByTemplates(context)
|
|
|
272
280
|
context.parent = getParentPath(context.element)
|
|
273
281
|
if (rendered) {
|
|
274
282
|
if (template) {
|
|
275
|
-
if (rendered?.[
|
|
283
|
+
if (rendered?.[DEP.TEMPLATE] != template) {
|
|
276
284
|
const clone = this.applyTemplate(context)
|
|
277
285
|
context.element.replaceChild(clone, rendered)
|
|
278
286
|
}
|
package/src/dom.mjs
CHANGED
|
@@ -2,15 +2,27 @@ import { signals, signal as stateSignal, notifyGet, notifySet, makeContext,
|
|
|
2
2
|
throttledEffect, effect, untracked, batch } from './state.mjs'
|
|
3
3
|
import { getValueByPath } from './bind.mjs'
|
|
4
4
|
import { setValueByPath, getProperties } from './bind.render.mjs'
|
|
5
|
+
import { DEP } from '../src/symbols.mjs'
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Tracks element => signal mapping so that each element only has one signal
|
|
9
|
+
*/
|
|
6
10
|
const domSignals = new WeakMap()
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Tracks element => mutationObservers
|
|
14
|
+
*/
|
|
15
|
+
const observers = new WeakMap()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A dom signal is a Proxy, to track access to properties
|
|
19
|
+
*/
|
|
8
20
|
const domSignalHandler = {
|
|
9
21
|
get: (target, property, receiver) => {
|
|
10
|
-
if (property===
|
|
22
|
+
if (property===DEP.XRAY) {
|
|
11
23
|
return target // don't notifyGet here, this is only called by set
|
|
12
24
|
}
|
|
13
|
-
if (property===
|
|
25
|
+
if (property===DEP.SIGNAL) {
|
|
14
26
|
return true
|
|
15
27
|
}
|
|
16
28
|
const value = target?.[property]
|
|
@@ -39,8 +51,17 @@ const domSignalHandler = {
|
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
/**
|
|
55
|
+
* This function returns a dom signal. Using this in an effect() function
|
|
56
|
+
* will automatically trigger the effect if a property of the dom signal
|
|
57
|
+
* changes.
|
|
58
|
+
* Valid options are any of the mutationObserver options, like characterData, subtree, etc.
|
|
59
|
+
* @param HTMLElement el
|
|
60
|
+
* @param Object options
|
|
61
|
+
* @returns Proxy
|
|
62
|
+
*/
|
|
42
63
|
export function signal(el, options) {
|
|
43
|
-
if (el[
|
|
64
|
+
if (el[DEP.XRAY]) {
|
|
44
65
|
return el
|
|
45
66
|
}
|
|
46
67
|
if (!signals.has(el)) {
|
|
@@ -50,8 +71,9 @@ export function signal(el, options) {
|
|
|
50
71
|
return signals.get(el)
|
|
51
72
|
}
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
/**
|
|
75
|
+
* This sets up the mutationObserver that calls notifySet on changes in the DOM
|
|
76
|
+
*/
|
|
55
77
|
function domListen(el, signal, options) {
|
|
56
78
|
const defaultOptions = {
|
|
57
79
|
characterData: true,
|
|
@@ -114,6 +136,11 @@ function domListen(el, signal, options) {
|
|
|
114
136
|
}
|
|
115
137
|
}
|
|
116
138
|
|
|
139
|
+
/**
|
|
140
|
+
* This function sets up the dom signal on an element, provided it has a `data-flow-list` attribute
|
|
141
|
+
* @param HTMLElement element - the element to track
|
|
142
|
+
* @returns Proxy
|
|
143
|
+
*/
|
|
117
144
|
export function trackDomList(element)
|
|
118
145
|
{
|
|
119
146
|
const path = this.getBindingPath(element)
|
|
@@ -149,8 +176,14 @@ export function trackDomList(element)
|
|
|
149
176
|
})
|
|
150
177
|
})
|
|
151
178
|
})
|
|
179
|
+
return s
|
|
152
180
|
}
|
|
153
181
|
|
|
182
|
+
/**
|
|
183
|
+
* This function sets up the dom signal on an element, provided it has a `data-flow-field` attribute
|
|
184
|
+
* @param HTMLElement element - the element to track
|
|
185
|
+
* @returns Proxy
|
|
186
|
+
*/
|
|
154
187
|
export function trackDomField(element, props, valueIsString) {
|
|
155
188
|
if (domSignals.has(element)) {
|
|
156
189
|
return
|
|
@@ -174,4 +207,5 @@ export function trackDomField(element, props, valueIsString) {
|
|
|
174
207
|
})
|
|
175
208
|
})
|
|
176
209
|
})
|
|
210
|
+
return s
|
|
177
211
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import '../flow.mjs'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
html: {
|
|
5
|
+
anchor: html`<div class="anchor"></div>`
|
|
6
|
+
},
|
|
7
|
+
css: {
|
|
8
|
+
anchor: css`
|
|
9
|
+
.anchor {
|
|
10
|
+
anchor-name: --cursor-anchor;
|
|
11
|
+
position: absolute;
|
|
12
|
+
z-index: 10000;
|
|
13
|
+
width: 10px;
|
|
14
|
+
height: 10px;
|
|
15
|
+
transform: rotate(45deg);
|
|
16
|
+
transform-origin: top left;
|
|
17
|
+
background: red;
|
|
18
|
+
display: none;
|
|
19
|
+
}`
|
|
20
|
+
},
|
|
21
|
+
actions: {
|
|
22
|
+
anchorPosition: function(position) {
|
|
23
|
+
this.state.anchor.position = position
|
|
24
|
+
},
|
|
25
|
+
anchorShow: function() {
|
|
26
|
+
this.state.anchor.visible = true
|
|
27
|
+
},
|
|
28
|
+
anchorHide: function() {
|
|
29
|
+
this.state.anchor.visible = false
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
hooks: {
|
|
33
|
+
start: function() {
|
|
34
|
+
this.container.insertAdjacentHTML('beforeend','<simply-render rel="anchor"></simply-render>')
|
|
35
|
+
this.state.anchor = {
|
|
36
|
+
element: this.container.querySelector('.anchor'),
|
|
37
|
+
offset: this.container.getBoundingClientRect(),
|
|
38
|
+
visible: false,
|
|
39
|
+
position: {
|
|
40
|
+
x: 0,
|
|
41
|
+
y: 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
this.state.anchor.element = this.container.querySelector('.anchor')
|
|
46
|
+
this.state.anchor.offset = this.container.getBoundingClientRect()
|
|
47
|
+
},100)
|
|
48
|
+
this.selectionListener = document.addEventListener('selectionchange', () => {
|
|
49
|
+
const selection = window.getSelection()
|
|
50
|
+
if (!selection.rangeCount || selection.isCollapsed) {
|
|
51
|
+
this.state.anchor.visible = false
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (!this.container.contains(selection.anchorNode)) {
|
|
55
|
+
this.state.anchor.visible = false
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (!selection.anchorNode.parentElement.closest('[contenteditable]')) {
|
|
59
|
+
this.state.anchor.visible = false
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
this.state.anchor.visible = true
|
|
63
|
+
const position = getCursorPosition(this.container)
|
|
64
|
+
this.state.anchor.position = position
|
|
65
|
+
})
|
|
66
|
+
simply.state.effect(() => {
|
|
67
|
+
const pos = this.state.anchor.position
|
|
68
|
+
const offset = this.state.anchor.offset
|
|
69
|
+
simply.state.batch(() => {
|
|
70
|
+
this.state.anchor.element.style.top = (pos.y + pos.height + offset.top) + 'px'
|
|
71
|
+
this.state.anchor.element.style.left = (pos.x + offset.left) + 'px'
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
simply.state.effect(() => {
|
|
75
|
+
const visible = this.state.anchor.visible
|
|
76
|
+
console.log('anchor visible ', visible)
|
|
77
|
+
if (visible) {
|
|
78
|
+
this.state.anchor.element.style.display = 'block'
|
|
79
|
+
} else {
|
|
80
|
+
this.state.anchor.element.style.display = 'none'
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* This function returns the cursor position and height, if the cursor is in
|
|
90
|
+
* the given element. The x and y position are calculated relative to the top
|
|
91
|
+
* left of the given element. This function does not alter the DOM in any way.
|
|
92
|
+
*/
|
|
93
|
+
function getCursorPosition(element) {
|
|
94
|
+
const selection = window.getSelection();
|
|
95
|
+
if (!selection.rangeCount) return null;
|
|
96
|
+
|
|
97
|
+
const range = document.createRange();
|
|
98
|
+
range.setStart(selection.focusNode, selection.focusOffset);
|
|
99
|
+
range.collapse(true);
|
|
100
|
+
|
|
101
|
+
// Try getClientRects() first — often non-empty even on empty lines
|
|
102
|
+
const elementRect = element.getBoundingClientRect();
|
|
103
|
+
|
|
104
|
+
const cursorNode = selection.focusNode;
|
|
105
|
+
const cursorElement = cursorNode.nodeType === Node.TEXT_NODE
|
|
106
|
+
? cursorNode.parentElement
|
|
107
|
+
: cursorNode;
|
|
108
|
+
|
|
109
|
+
let x,y,height;
|
|
110
|
+
const rects = range.getClientRects();
|
|
111
|
+
if (rects.length > 0) {
|
|
112
|
+
x = rects[0].left - elementRect.left
|
|
113
|
+
y = rects[0].top - elementRect.top
|
|
114
|
+
height = rects[0].height
|
|
115
|
+
} else {
|
|
116
|
+
// Fallback for truly empty element: use padding from CSS
|
|
117
|
+
const style = window.getComputedStyle(cursorElement);
|
|
118
|
+
const lineHeight = parseFloat(style.lineHeight);
|
|
119
|
+
height = isNaN(lineHeight) ? parseFloat(style.fontSize) : lineHeight
|
|
120
|
+
const cursorElementRect = cursorElement.getBoundingClientRect();
|
|
121
|
+
x = cursorElementRect.left - elementRect.left + parseFloat(style.paddingLeft)
|
|
122
|
+
y = cursorElementRect.top - elementRect.top + parseFloat(style.paddingTop)
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
x,
|
|
126
|
+
y,
|
|
127
|
+
height,
|
|
128
|
+
element: cursorElement
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import anchor from './anchor.mjs'
|
|
2
|
+
import '../flow.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const simplyToolbarCSS = css`
|
|
6
|
+
:host {
|
|
7
|
+
--simply-button-font: arial, helvetica, sans-serif;
|
|
8
|
+
--simply-button-font-size: 11px;
|
|
9
|
+
--simply-button-width: 50px;
|
|
10
|
+
--simply-button-height: 50px;
|
|
11
|
+
--simply-button-color: #333;
|
|
12
|
+
--simply-button-primary: #ea5922;
|
|
13
|
+
}
|
|
14
|
+
.simply-button {
|
|
15
|
+
height: var(--simply-button-height);
|
|
16
|
+
border-top: 1px solid transparent;
|
|
17
|
+
border-bottom: 2px solid transparent;
|
|
18
|
+
transition: background 0.2s ease;
|
|
19
|
+
font-size: var(--simply-button-font-size);
|
|
20
|
+
letter-spacing: 0;
|
|
21
|
+
font-family: var(--simply-button-font);
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
user-select: none;
|
|
24
|
+
vertical-align: top;
|
|
25
|
+
min-width: var(--simply-button-width);
|
|
26
|
+
text-align: center;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
padding: 0 4px;
|
|
29
|
+
text-transform: none;
|
|
30
|
+
background: transparent;
|
|
31
|
+
outline: none;
|
|
32
|
+
box-shadow: none;
|
|
33
|
+
border-radius: 0;
|
|
34
|
+
color: var(--simply-button-color);
|
|
35
|
+
position: relative;
|
|
36
|
+
}
|
|
37
|
+
.simply-button:hover {
|
|
38
|
+
border-bottom: 2px solid var(--simply-button-primary);
|
|
39
|
+
box-shadow: none;
|
|
40
|
+
}
|
|
41
|
+
.simply-button .ds-icon {
|
|
42
|
+
height: 26px;
|
|
43
|
+
font-size: 26px;
|
|
44
|
+
padding: 0 4px;
|
|
45
|
+
display: block;
|
|
46
|
+
margin: -2px auto -2px;
|
|
47
|
+
position: relative;
|
|
48
|
+
}
|
|
49
|
+
.simply-button.ds-selected {
|
|
50
|
+
border-top-color: var(--ds-grey-40);
|
|
51
|
+
background-color: var(--ds-grey-light);
|
|
52
|
+
border-left: 1px solid var(--ds-grey-40);
|
|
53
|
+
border-right: 1px solid var(--ds-white);
|
|
54
|
+
}
|
|
55
|
+
.simply-button:active {
|
|
56
|
+
border-bottom: 2px solid var(--ds-primary);
|
|
57
|
+
box-shadow: none;
|
|
58
|
+
}
|
|
59
|
+
.simply-toolbar {
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
min-width: 100%;
|
|
62
|
+
min-height: 50px;
|
|
63
|
+
display: flex;
|
|
64
|
+
position: relative;
|
|
65
|
+
}
|
|
66
|
+
.simply-toolbar-main {
|
|
67
|
+
border-top: 2px solid var(--simply-button-primary);
|
|
68
|
+
background: linear-gradient(180deg, white 0, white 95%, #CCC 100%);
|
|
69
|
+
}
|
|
70
|
+
.simply-toolbar-inline {
|
|
71
|
+
min-width: 100px;
|
|
72
|
+
}
|
|
73
|
+
.simply-toolbar-inline .ds-button,
|
|
74
|
+
.simply-toolbar .ds-button {
|
|
75
|
+
margin: 0;
|
|
76
|
+
}
|
|
77
|
+
.simply-toolbar-sub .simply-toolbar {
|
|
78
|
+
background: #EEE;
|
|
79
|
+
min-height: 40px;
|
|
80
|
+
}
|
|
81
|
+
.simply-toolbar-sub .simply-button {
|
|
82
|
+
height: 40px;
|
|
83
|
+
min-width: 40px;
|
|
84
|
+
}
|
|
85
|
+
.simply-toolbar-sub .simply-button .ds-icon {
|
|
86
|
+
height: 20px;
|
|
87
|
+
font-size: 20px;
|
|
88
|
+
}
|
|
89
|
+
.simply-toolbar-highlight {
|
|
90
|
+
background: var(--ds-primary-gradient-bump);
|
|
91
|
+
color: var(--ds-primary-contrast);
|
|
92
|
+
}
|
|
93
|
+
.simply-toolbar .simply-toolbar-title {
|
|
94
|
+
margin-top: 0;
|
|
95
|
+
}
|
|
96
|
+
.simply-toolbar-spacer {
|
|
97
|
+
border-left: 1px solid #ccc;
|
|
98
|
+
height: 60px;
|
|
99
|
+
position: absolute;
|
|
100
|
+
display: inline-block;
|
|
101
|
+
}
|
|
102
|
+
.simply-button-expands:not(.ds-selected)::after {
|
|
103
|
+
content: "";
|
|
104
|
+
display: block;
|
|
105
|
+
position: absolute;
|
|
106
|
+
bottom: 2px;
|
|
107
|
+
left: 50%;
|
|
108
|
+
margin-left: -3px;
|
|
109
|
+
width: 0;
|
|
110
|
+
border-top: 3px solid #888;
|
|
111
|
+
border-bottom: 0;
|
|
112
|
+
border-left: 3px solid transparent;
|
|
113
|
+
border-right: 3px solid transparent;
|
|
114
|
+
}
|
|
115
|
+
.simply-button-expanded {
|
|
116
|
+
background: #EEE;
|
|
117
|
+
}
|
|
118
|
+
.simply-button.simply-button-expanded::after {
|
|
119
|
+
display: none;
|
|
120
|
+
}
|
|
121
|
+
.simply-toolbar .simply-push-right {
|
|
122
|
+
margin-left: auto;
|
|
123
|
+
}
|
|
124
|
+
.simply-toolbar input[type="text"] {
|
|
125
|
+
margin-right:0;
|
|
126
|
+
margin-bottom: 0;
|
|
127
|
+
margin-top: 10px;
|
|
128
|
+
font-size: small;
|
|
129
|
+
line-height: 1.2em;
|
|
130
|
+
height: 35px;
|
|
131
|
+
}
|
|
132
|
+
.simply-toolbar-header {
|
|
133
|
+
border-top-width: 5px;
|
|
134
|
+
}
|
|
135
|
+
.ds-nightmode .simply-toolbar {
|
|
136
|
+
background: linear-gradient(var(--ds-grey-90) 0%, var(--ds-grey-90) 95%, black 100%);
|
|
137
|
+
color: var(--ds-white);
|
|
138
|
+
}
|
|
139
|
+
.ds-nightmode .simply-button {
|
|
140
|
+
color: var(--ds-white);
|
|
141
|
+
}
|
|
142
|
+
.ds-nightmode .simply-button.ds-selected {
|
|
143
|
+
background-color: var(--ds-grey-80);
|
|
144
|
+
border-left-color: var(--ds-black);
|
|
145
|
+
border-top-color: var(--ds-black);
|
|
146
|
+
border-right-color: var(--ds-grey-60);
|
|
147
|
+
}
|
|
148
|
+
.ds-nightmode .simply-button[disabled] {
|
|
149
|
+
background-color: transparent;
|
|
150
|
+
color: var(--ds-grey-60);
|
|
151
|
+
}
|
|
152
|
+
.simply-toolbar.ds-hidden {
|
|
153
|
+
height: 0px;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
min-height: 0px;
|
|
156
|
+
}`
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
//TODO: allow app to specify which toolbar to show instead of fixed toolbars.floatToolbarText.buttons
|
|
160
|
+
const simplyToolbarContents = html`
|
|
161
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@muze-nl/theds@0.2.7/dist/theds.css">
|
|
162
|
+
<style>
|
|
163
|
+
${simplyToolbarCSS}
|
|
164
|
+
</style>
|
|
165
|
+
<nav class="simply-toolbar simply-toolbar-main simply-toolbar-inline" data-flow-list="toolbars.floatToolbarText.buttons">
|
|
166
|
+
<template rel="simply-toolbar"></template>
|
|
167
|
+
</nav>
|
|
168
|
+
<div class="simply-toolbar-sub" data-flow-map="toolbars.floatToolbarText.toolbars">
|
|
169
|
+
<template>
|
|
170
|
+
<nav class="simply-toolbar ds-hidden" data-flow-field=":key" data-flow-transform="simplyToolbar">
|
|
171
|
+
<div data-flow-list="buttons">
|
|
172
|
+
<template rel="simply-toolbar"></template>
|
|
173
|
+
</div>
|
|
174
|
+
</nav>
|
|
175
|
+
</template>
|
|
176
|
+
</div>`
|
|
177
|
+
|
|
178
|
+
export default {
|
|
179
|
+
css: {
|
|
180
|
+
simplyToolbarFloat: css`
|
|
181
|
+
:root {
|
|
182
|
+
--ds-shadow-light: rgba(0,0,0,0.07);
|
|
183
|
+
--ds-shadow-middle: rgba(0,0,0,0.09);
|
|
184
|
+
--ds-shadow-dark: rgba(0,0,0,0.11);
|
|
185
|
+
--ds-shadow-small:
|
|
186
|
+
0 1px 1px var(--ds-shadow-dark),
|
|
187
|
+
0 2px 2px var(--ds-shadow-middle),
|
|
188
|
+
0 4px 4px var(--ds-shadow-light)
|
|
189
|
+
;
|
|
190
|
+
}
|
|
191
|
+
.simply-toolbar-float {
|
|
192
|
+
margin: 0;
|
|
193
|
+
padding: 0;
|
|
194
|
+
border: 0;
|
|
195
|
+
width: auto;
|
|
196
|
+
position-anchor: --cursor-anchor;
|
|
197
|
+
position-area: end span-all;
|
|
198
|
+
position: absolute;
|
|
199
|
+
min-width:100px;
|
|
200
|
+
min-height: 50px;
|
|
201
|
+
background: white;
|
|
202
|
+
z-index: 10000;
|
|
203
|
+
margin-top: -4px;
|
|
204
|
+
box-shadow: var(--ds-shadow-small);
|
|
205
|
+
}`
|
|
206
|
+
},
|
|
207
|
+
html: {
|
|
208
|
+
'simply-toolbar':
|
|
209
|
+
html`<button class="ds-button simply-button" data-flow-field=":value" data-flow-transform="simplyToolbarButton">
|
|
210
|
+
<svg class="ds-icon ds-icon-feather">
|
|
211
|
+
<use xlink:href="feather-sprite.svg#x" data-flow-transform="simplyIcon" data-flow-field="icon">
|
|
212
|
+
</use></svg>
|
|
213
|
+
<span data-flow-field="label"></span>
|
|
214
|
+
</button>`,
|
|
215
|
+
'simply-toolbar-float':
|
|
216
|
+
html`<div class="simply-toolbar simply-toolbar-float simply-toolbar-inline" popover="manual"></div>`
|
|
217
|
+
},
|
|
218
|
+
transformers: {
|
|
219
|
+
simplyToolbar: function(context, next) {
|
|
220
|
+
context.element.id = context.value
|
|
221
|
+
},
|
|
222
|
+
simplyToolbarButton: function(context, next) {
|
|
223
|
+
const el = context.element
|
|
224
|
+
el.value = context.value.command
|
|
225
|
+
if (context.value.command=="expand") {
|
|
226
|
+
el.classList.add('simply-button-expands')
|
|
227
|
+
}
|
|
228
|
+
if (context.value.command) {
|
|
229
|
+
el.dataset.simplyCommand = context.value.command
|
|
230
|
+
}
|
|
231
|
+
if (context.value.value) {
|
|
232
|
+
el.value = context.value.value
|
|
233
|
+
}
|
|
234
|
+
// skip next()
|
|
235
|
+
},
|
|
236
|
+
simplyIcon: function(context, next) {
|
|
237
|
+
const url = new URL(context.element.getAttribute('xlink:href'), document.location)
|
|
238
|
+
url.hash = context.value
|
|
239
|
+
context.element.setAttribute('xlink:href', url.href)
|
|
240
|
+
// skip next()
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
commands: {
|
|
244
|
+
toggle: function(el, value) {
|
|
245
|
+
|
|
246
|
+
},
|
|
247
|
+
align: function(el, value) {
|
|
248
|
+
|
|
249
|
+
},
|
|
250
|
+
expand: function(el, value) {
|
|
251
|
+
const toolbar = el.closest('.simply-toolbar')
|
|
252
|
+
const subToolbars = toolbar.nextElementSibling;
|
|
253
|
+
if (!subToolbars) {
|
|
254
|
+
console.error('no subtoolbars')
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
const current = Array.from(subToolbars.querySelectorAll('.simply-toolbar:not(.ds-hidden)'))
|
|
258
|
+
for( let t of current) {
|
|
259
|
+
t.classList.add('ds-hidden')
|
|
260
|
+
}
|
|
261
|
+
const selectedToolbar = subToolbars.querySelector('#'+value)
|
|
262
|
+
if (selectedToolbar) {
|
|
263
|
+
selectedToolbar.classList.remove('ds-hidden')
|
|
264
|
+
const buttons = Array.from(toolbar.querySelectorAll('.simply-button-expanded'))
|
|
265
|
+
for (let button of buttons) {
|
|
266
|
+
button.classList.remove('simply-button-expanded')
|
|
267
|
+
}
|
|
268
|
+
el.classList.add('simply-button-expanded')
|
|
269
|
+
} else {
|
|
270
|
+
console.error('toolbar '+value+' not found')
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
actions: {
|
|
275
|
+
showToolbar: function(position) {
|
|
276
|
+
this.state.toolbar.showPopover()
|
|
277
|
+
},
|
|
278
|
+
hideToolbar: function() {
|
|
279
|
+
this.state.toolbar.hidePopover()
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
hooks: {
|
|
283
|
+
start: function() {
|
|
284
|
+
this.state.toolbar = this.container.querySelector('simply-edit-focus-toolbar')
|
|
285
|
+
if (!this.state.toolbar) {
|
|
286
|
+
this.container.insertAdjacentHTML('beforeend','<simply-render rel="simply-toolbar-float"></simply-render>')
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
const toolbar = document.querySelector('.simply-toolbar-float')
|
|
289
|
+
const shadow = toolbar.attachShadow({ mode: "open"})
|
|
290
|
+
shadow.innerHTML = simplyToolbarContents
|
|
291
|
+
this.state.toolbar = toolbar
|
|
292
|
+
simply.state.effect(() => {
|
|
293
|
+
let visible = this.state.anchor.visible
|
|
294
|
+
if (visible) {
|
|
295
|
+
this.actions.showToolbar()
|
|
296
|
+
} else {
|
|
297
|
+
this.actions.hideToolbar()
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
// databinding doesn't reach into shadowRoot by default, so set it up here
|
|
301
|
+
simply.bind({
|
|
302
|
+
root: this.state,
|
|
303
|
+
container: toolbar.shadowRoot,
|
|
304
|
+
transformers: this.transformers
|
|
305
|
+
})
|
|
306
|
+
// same for commands, set container explicitly to the shadowRoot
|
|
307
|
+
simply.command({ app: this, container: toolbar.shadowRoot, commands: this.commands})
|
|
308
|
+
}, 100)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
components: {
|
|
313
|
+
anchor
|
|
314
|
+
}
|
|
315
|
+
}
|