@shawcx/avispa 1.0.0 → 2.0.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/README.md +117 -5
- package/css/Avispa.css +6 -1
- package/css/Avispa.css.map +1 -1
- package/js/Avispa.js +66 -43
- package/js/BaseObject.js +56 -10
- package/js/Group.js +114 -5
- package/js/Link.js +75 -26
- package/js/Node.js +6 -2
- package/js/Socket.js +134 -0
- package/js/Types.js +1 -1
- package/js/Utils.js +12 -4
- package/package.json +27 -6
- package/scss/Avispa.scss +7 -1
package/README.md
CHANGED
|
@@ -1,11 +1,123 @@
|
|
|
1
1
|
Avispa
|
|
2
2
|
======
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Draw node/link graphs in SVG and interact with them: drag nodes and groups, pan,
|
|
5
|
+
zoom, bend links, and resize, nest, and collapse groups, with mouse, pen, or touch.
|
|
6
|
+
Items are Backbone views, and an optional WebSocket feed keeps a graph live.
|
|
5
7
|
|
|
6
|
-
Avispa
|
|
7
|
-
general purpose Javascript library for rendering graphs.
|
|
8
|
+
Avispa is Spanish for wasp.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Demo at [https://shawcx.github.io/avispa/](https://shawcx.github.io/avispa/)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
Install
|
|
13
|
+
-------
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @shawcx/avispa jquery underscore backbone
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
jQuery (3 or 4), Underscore, and Backbone are peer dependencies, so your app
|
|
20
|
+
supplies one copy of each. The library is CommonJS (`js/Avispa.js`), meant to be
|
|
21
|
+
bundled with browserify, webpack, esbuild, or similar. The theme is
|
|
22
|
+
`css/Avispa.css`, or `@use` `scss/Avispa.scss` from your own Sass.
|
|
23
|
+
|
|
24
|
+
Page setup
|
|
25
|
+
----------
|
|
26
|
+
|
|
27
|
+
Avispa draws into an SVG on your page, which needs these layers, and an `#Arrow`
|
|
28
|
+
marker for the link arrowheads:
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<svg class="avispa">
|
|
32
|
+
<defs>
|
|
33
|
+
<marker id="Arrow" viewBox="0 0 10 10" refX="7" refY="5"
|
|
34
|
+
markerWidth="4" markerHeight="4" orient="auto">
|
|
35
|
+
<path d="M 1 1 L 9 5 L 1 9 z" />
|
|
36
|
+
</marker>
|
|
37
|
+
</defs>
|
|
38
|
+
<g class="pan">
|
|
39
|
+
<g class="zoom">
|
|
40
|
+
<g class="groups"></g>
|
|
41
|
+
<g class="links"></g>
|
|
42
|
+
<g class="objects"></g>
|
|
43
|
+
<g class="labels"></g>
|
|
44
|
+
</g>
|
|
45
|
+
</g>
|
|
46
|
+
</svg>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Usage
|
|
50
|
+
-----
|
|
51
|
+
|
|
52
|
+
Each item takes a `Position` model; moving an item means setting its position, and
|
|
53
|
+
the view redraws. A child's `x` and `y` are relative to its parent group.
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const $ = require('jquery')
|
|
57
|
+
const Avispa = require('@shawcx/avispa')
|
|
58
|
+
|
|
59
|
+
$(() => {
|
|
60
|
+
const graph = new Avispa.Avispa({ el: $('svg.avispa') })
|
|
61
|
+
|
|
62
|
+
const group = new Avispa.Group({
|
|
63
|
+
position: new Avispa.Position({ x: -200, y: -75, w: 200, h: 150 }),
|
|
64
|
+
})
|
|
65
|
+
graph.$groups.append(group.$el)
|
|
66
|
+
|
|
67
|
+
const a = new Avispa.Node({
|
|
68
|
+
label: 'A',
|
|
69
|
+
parent: group,
|
|
70
|
+
position: new Avispa.Position({ x: 60, y: 75, radius: 30 }),
|
|
71
|
+
})
|
|
72
|
+
const b = new Avispa.Node({
|
|
73
|
+
label: 'B',
|
|
74
|
+
position: new Avispa.Position({ x: 150, y: 0, radius: 30 }),
|
|
75
|
+
})
|
|
76
|
+
graph.$objects.append(a.$el, b.$el)
|
|
77
|
+
|
|
78
|
+
new Avispa.Link({ parent: graph, left: a, right: b })
|
|
79
|
+
|
|
80
|
+
// a.position.set({ x: 80 }) moves A and its link
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
On screen:
|
|
85
|
+
|
|
86
|
+
- Drag the background to pan, scroll to zoom, middle-click to recenter.
|
|
87
|
+
- Drag a node or group to move it; a group's children come along. A node stays
|
|
88
|
+
inside its group, and a nested group dragged past its parent's edge grows it.
|
|
89
|
+
- Drag a group's bottom-right corner to resize it, down to what its contents need.
|
|
90
|
+
- Drag a link to bend it; shift-click it to straighten it.
|
|
91
|
+
|
|
92
|
+
In code, `group.Collapse()`, `Expand()`, and `ToggleCollapse()` shrink a group to
|
|
93
|
+
its title bar and hide what's inside, and `item.remove()` takes an item off the
|
|
94
|
+
graph (remove its links yourself).
|
|
95
|
+
|
|
96
|
+
To customize, subclass `Avispa`, `Group`, `Node`, and `Link`, and override
|
|
97
|
+
`initialize` (calling `super`), `render`, and the `LeftClick`, `MiddleClick`,
|
|
98
|
+
`RightClick`, and `OnContextMenu` hooks. [`example/src/example.js`](example/src/example.js)
|
|
99
|
+
does this, adding a title to each group that collapses it on double-click.
|
|
100
|
+
|
|
101
|
+
### WebSocket
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
class Graph extends Avispa.Avispa {
|
|
105
|
+
OnMessage(msg) {
|
|
106
|
+
// each JSON message from ws(s)://<page host>/ws
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const graph = new Graph({ el: $('svg.avispa'), socket: 'ws' })
|
|
111
|
+
graph.socket.send({ hello: 'world' })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The socket reconnects with backoff, keeps the connection alive with pings, and
|
|
115
|
+
queues anything sent before it opens. Pass an options object instead of a path
|
|
116
|
+
for an explicit `url` or your own `onopen` / `onclose` / `onmessage` callbacks.
|
|
117
|
+
|
|
118
|
+
Development
|
|
119
|
+
-----------
|
|
120
|
+
|
|
121
|
+
- `npm run example` builds the demo in `example/` and opens it in a browser.
|
|
122
|
+
- `npm test` runs the Playwright tests in Google Chrome.
|
|
123
|
+
- `npm run build` compiles `scss/Avispa.scss` to `css/Avispa.css`.
|
package/css/Avispa.css
CHANGED
|
@@ -9,9 +9,14 @@ g.group rect.hover {
|
|
|
9
9
|
stroke: #333;
|
|
10
10
|
stroke-width: 2px;
|
|
11
11
|
}
|
|
12
|
+
g.group rect.handle {
|
|
13
|
+
cursor: nwse-resize;
|
|
14
|
+
fill: #999;
|
|
15
|
+
stroke: none;
|
|
16
|
+
}
|
|
12
17
|
g.group text {
|
|
13
18
|
cursor: pointer;
|
|
14
|
-
font: bold
|
|
19
|
+
font: bold 10pt "Andale Mono", AndaleMono, monospace;
|
|
15
20
|
fill: #000;
|
|
16
21
|
text-anchor: left;
|
|
17
22
|
}
|
package/css/Avispa.css.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sourceRoot":"","sources":["../scss/Avispa.scss"],"names":[],"mappings":"AAMI;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EAEA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA","file":"Avispa.css"}
|
|
1
|
+
{"version":3,"sourceRoot":"","sources":["../scss/Avispa.scss"],"names":[],"mappings":"AAMI;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EAEA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA","file":"Avispa.css"}
|
package/js/Avispa.js
CHANGED
|
@@ -3,45 +3,34 @@
|
|
|
3
3
|
const _ = require('underscore')
|
|
4
4
|
const Backbone = require('backbone')
|
|
5
5
|
|
|
6
|
-
const { Position }
|
|
7
|
-
const { cancelEvent, $SVG } = require('./Utils.js')
|
|
6
|
+
const { Position } = require('./Types.js')
|
|
7
|
+
const { cancelEvent, $SVG, normalizeWheel } = require('./Utils.js')
|
|
8
8
|
|
|
9
9
|
const context = require('./Context.js')
|
|
10
10
|
const Group = require('./Group.js')
|
|
11
11
|
const Node = require('./Node.js')
|
|
12
12
|
const Link = require('./Link.js')
|
|
13
|
-
|
|
14
|
-
let normalizeWheel = function(event) {
|
|
15
|
-
if (event.wheelDelta) {
|
|
16
|
-
return event.wheelDelta / 120
|
|
17
|
-
}
|
|
18
|
-
if (event.detail) {
|
|
19
|
-
return event.detail / -3
|
|
20
|
-
}
|
|
21
|
-
return 0
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const $SVG = function(name) {
|
|
26
|
-
return $(document.createElementNS('http://www.w3.org/2000/svg', name))
|
|
27
|
-
}
|
|
28
|
-
|
|
13
|
+
const Socket = require('./Socket.js')
|
|
29
14
|
|
|
30
15
|
class Avispa extends Backbone.View {
|
|
31
16
|
events() {
|
|
32
17
|
return {
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'contextmenu.avispa'
|
|
18
|
+
'pointerdown.avispa' : 'OnMouseDown',
|
|
19
|
+
'pointermove.avispa' : 'OnMouseMove',
|
|
20
|
+
'pointerup.avispa' : 'OnMouseUp',
|
|
21
|
+
'pointercancel.avispa' : 'OnPointerCancel',
|
|
22
|
+
'wheel.avispa' : 'OnMouseWheel',
|
|
23
|
+
'contextmenu.avispa' : 'OnContextMenu',
|
|
39
24
|
}
|
|
40
25
|
}
|
|
41
26
|
|
|
42
27
|
initialize(options) {
|
|
43
28
|
context.a = this
|
|
44
29
|
|
|
30
|
+
// the surface handles its own drags: keep touch from panning the page
|
|
31
|
+
// and drags from selecting text
|
|
32
|
+
this.$el.css({'touch-action': 'none', 'user-select': 'none'})
|
|
33
|
+
|
|
45
34
|
_.bindAll(this, 'render',
|
|
46
35
|
'OnMouseDown', 'OnMouseMove', 'OnMouseUp', 'OnMouseWheel', 'OnContextMenu')
|
|
47
36
|
|
|
@@ -76,6 +65,28 @@ class Avispa extends Backbone.View {
|
|
|
76
65
|
this.$pan.y = parseInt(window.innerHeight / 2)
|
|
77
66
|
|
|
78
67
|
this.Pan(0,0)
|
|
68
|
+
|
|
69
|
+
// opt-in WebSocket transport: `socket` may be a path string or a
|
|
70
|
+
// Socket options object. Incoming messages are routed to OnMessage
|
|
71
|
+
// unless the caller supplies its own onmessage handler.
|
|
72
|
+
if (options && options.socket) {
|
|
73
|
+
let socketOptions = (typeof options.socket === 'string')
|
|
74
|
+
? { path: options.socket }
|
|
75
|
+
: Object.assign({}, options.socket)
|
|
76
|
+
|
|
77
|
+
if (!socketOptions.onmessage) {
|
|
78
|
+
socketOptions.onmessage = (msg) => this.OnMessage(msg)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.socket = new Socket(socketOptions)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
remove() {
|
|
86
|
+
if (this.socket) {
|
|
87
|
+
this.socket.close()
|
|
88
|
+
}
|
|
89
|
+
return super.remove()
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
Pan(dx, dy) {
|
|
@@ -96,9 +107,8 @@ class Avispa extends Backbone.View {
|
|
|
96
107
|
|
|
97
108
|
Zoom(delta) {
|
|
98
109
|
let scale = (delta == 0) ? 1.0 : this.scale + delta * this.zoom.step
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
110
|
+
scale = Math.min(this.zoom.max, Math.max(this.zoom.min, scale))
|
|
111
|
+
this.Scale(scale)
|
|
102
112
|
return this
|
|
103
113
|
}
|
|
104
114
|
|
|
@@ -123,14 +133,14 @@ class Avispa extends Backbone.View {
|
|
|
123
133
|
return cancelEvent(event)
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
switch (event.
|
|
127
|
-
case
|
|
136
|
+
switch (event.button) {
|
|
137
|
+
case 0:
|
|
128
138
|
this.LeftDown(event)
|
|
129
139
|
break
|
|
130
|
-
case
|
|
140
|
+
case 1:
|
|
131
141
|
this.MiddleDown(event)
|
|
132
142
|
break
|
|
133
|
-
case
|
|
143
|
+
case 2:
|
|
134
144
|
if (this.RightDown) {
|
|
135
145
|
this.RightDown(event)
|
|
136
146
|
}
|
|
@@ -174,33 +184,34 @@ class Avispa extends Backbone.View {
|
|
|
174
184
|
this.offset = null
|
|
175
185
|
|
|
176
186
|
if (this.dragItem) {
|
|
187
|
+
// click hooks are optional on drag items (Link has no Middle/RightClick)
|
|
177
188
|
if (this.dragItem.jitter < 3) {
|
|
178
|
-
switch (event.
|
|
189
|
+
switch (event.button) {
|
|
190
|
+
case 0:
|
|
191
|
+
if (this.dragItem.LeftClick) this.dragItem.LeftClick(event)
|
|
192
|
+
break
|
|
179
193
|
case 1:
|
|
180
|
-
this.dragItem.
|
|
194
|
+
if (this.dragItem.MiddleClick) this.dragItem.MiddleClick(event)
|
|
181
195
|
break
|
|
182
196
|
case 2:
|
|
183
|
-
this.dragItem.
|
|
184
|
-
break
|
|
185
|
-
case 3:
|
|
186
|
-
this.dragItem.RightClick(event)
|
|
197
|
+
if (this.dragItem.RightClick) this.dragItem.RightClick(event)
|
|
187
198
|
break
|
|
188
199
|
}
|
|
189
200
|
}
|
|
190
|
-
|
|
201
|
+
if (this.dragItem.MouseUp) {
|
|
191
202
|
this.dragItem.MouseUp(event)
|
|
192
203
|
}
|
|
193
204
|
this.dragItem = null
|
|
194
205
|
}
|
|
195
206
|
else {
|
|
196
|
-
switch (event.
|
|
197
|
-
case
|
|
207
|
+
switch (event.button) {
|
|
208
|
+
case 0:
|
|
198
209
|
this.LeftClick(event)
|
|
199
210
|
break
|
|
200
|
-
case
|
|
211
|
+
case 1:
|
|
201
212
|
this.MiddleClick(event)
|
|
202
213
|
break
|
|
203
|
-
case
|
|
214
|
+
case 2:
|
|
204
215
|
this.RightClick(event)
|
|
205
216
|
break
|
|
206
217
|
}
|
|
@@ -209,8 +220,18 @@ class Avispa extends Backbone.View {
|
|
|
209
220
|
return cancelEvent(event)
|
|
210
221
|
}
|
|
211
222
|
|
|
223
|
+
OnPointerCancel(event) {
|
|
224
|
+
// the browser took the pointer over: drop any pan or drag in progress
|
|
225
|
+
this.offset = null
|
|
226
|
+
this.dragItem = null
|
|
227
|
+
}
|
|
228
|
+
|
|
212
229
|
OnMouseWheel(event) {
|
|
213
|
-
|
|
230
|
+
// a zero delta (a horizontal-only scroll) would reset the zoom to 1.0
|
|
231
|
+
let delta = normalizeWheel(event)
|
|
232
|
+
if (delta) {
|
|
233
|
+
this.Zoom(delta)
|
|
234
|
+
}
|
|
214
235
|
//this.$('#zoomslider').slider('option', 'value', this.scale)
|
|
215
236
|
return cancelEvent(event)
|
|
216
237
|
}
|
|
@@ -219,6 +240,7 @@ class Avispa extends Backbone.View {
|
|
|
219
240
|
MiddleClick(event) {}
|
|
220
241
|
RightClick(event) {}
|
|
221
242
|
OnContextMenu(event) {}
|
|
243
|
+
OnMessage(msg) {}
|
|
222
244
|
}
|
|
223
245
|
|
|
224
246
|
exports.Avispa = Avispa
|
|
@@ -226,5 +248,6 @@ exports.Position = Position
|
|
|
226
248
|
exports.Group = Group
|
|
227
249
|
exports.Node = Node
|
|
228
250
|
exports.Link = Link
|
|
251
|
+
exports.Socket = Socket
|
|
229
252
|
exports.$SVG = $SVG
|
|
230
253
|
exports.cancelEvent = cancelEvent
|
package/js/BaseObject.js
CHANGED
|
@@ -8,13 +8,20 @@ const context = require('./Context.js')
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class BaseObject extends Backbone.View {
|
|
11
|
+
// render once the whole initialize chain has run, so a subclass's own
|
|
12
|
+
// elements exist before its render does
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super(options)
|
|
15
|
+
this.render()
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
events() {
|
|
12
19
|
return {
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'contextmenu'
|
|
20
|
+
'pointerdown' : 'OnMouseDown',
|
|
21
|
+
'pointerenter' : 'OnMouseEnter',
|
|
22
|
+
'pointerleave' : 'OnMouseLeave',
|
|
23
|
+
'pointerup' : 'OnMouseUp',
|
|
24
|
+
'contextmenu' : 'OnContextMenu',
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
|
|
@@ -26,22 +33,61 @@ class BaseObject extends Backbone.View {
|
|
|
26
33
|
_.bindAll(this, 'OnMouseDown', 'OnMouseUp', 'OnContextMenu', 'render')
|
|
27
34
|
|
|
28
35
|
this.parent = options.parent
|
|
36
|
+
this.children = []
|
|
29
37
|
this.position = options.position
|
|
30
|
-
if (this.position) {
|
|
31
|
-
this.position.bind('change', this.render, this)
|
|
32
|
-
}
|
|
33
38
|
|
|
34
39
|
if (this.parent) {
|
|
40
|
+
this.parent.children.push(this)
|
|
35
41
|
this.offset = {
|
|
36
42
|
x: this.position.get('x'),
|
|
37
43
|
y: this.position.get('y'),
|
|
38
44
|
}
|
|
39
45
|
this.ParentDrag(this.parent.position)
|
|
40
|
-
this.parent.position
|
|
46
|
+
this.listenTo(this.parent.position, 'change', this.ParentDrag)
|
|
47
|
+
this.$el.css('display', this.CollapsedAncestor() ? 'none' : '')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// bound after the initial ParentDrag: a subclass is still building its
|
|
51
|
+
// elements here, and the constructor renders once initialize is done
|
|
52
|
+
if (this.position) {
|
|
53
|
+
this.listenTo(this.position, 'change', this.render)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// stopListening (in Backbone's remove) drops the model listeners; leaving
|
|
58
|
+
// the parent's children keeps a removed item out of its sizing and collapse
|
|
59
|
+
remove() {
|
|
60
|
+
if (this.parent) {
|
|
61
|
+
this.parent.children = _.without(this.parent.children, this)
|
|
62
|
+
}
|
|
63
|
+
return super.remove()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// the outermost collapsed group containing this item, or null if it is on screen
|
|
67
|
+
CollapsedAncestor() {
|
|
68
|
+
let collapsed = null
|
|
69
|
+
for (let p = this.parent; p; p = p.parent) {
|
|
70
|
+
if (p.position.get('collapsed')) {
|
|
71
|
+
collapsed = p
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return collapsed
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// how far this item reaches into its parent group, from the parent's top-left
|
|
78
|
+
Extent() {
|
|
79
|
+
return [this.offset.x, this.offset.y]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// hide everything inside a collapsed group, and show it again on expand
|
|
83
|
+
UpdateVisibility() {
|
|
84
|
+
for (const child of this.children) {
|
|
85
|
+
child.$el.css('display', child.CollapsedAncestor() ? 'none' : '')
|
|
86
|
+
child.UpdateVisibility()
|
|
41
87
|
}
|
|
42
88
|
}
|
|
43
89
|
|
|
44
|
-
|
|
90
|
+
Size(w,h) {
|
|
45
91
|
this.position.set({w:w,h:h})
|
|
46
92
|
return this
|
|
47
93
|
}
|
package/js/Group.js
CHANGED
|
@@ -9,10 +9,24 @@ const BaseObject = require('./BaseObject.js')
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Group extends BaseObject {
|
|
12
|
+
// a collapsed group shrinks to its title bar: the 30px a child group keeps
|
|
13
|
+
// clear at the top in Drag
|
|
14
|
+
static COLLAPSED_HEIGHT = 30
|
|
15
|
+
|
|
16
|
+
// the resize handle in the bottom-right corner, and the smallest a group gets
|
|
17
|
+
static HANDLE = 10
|
|
18
|
+
static MIN_SIZE = 40
|
|
19
|
+
|
|
12
20
|
el() {
|
|
13
21
|
return $SVG('g').attr('class', 'group')
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
events() {
|
|
25
|
+
return Object.assign(super.events(), {
|
|
26
|
+
'pointerdown rect.handle' : 'OnHandleDown',
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
initialize(options) {
|
|
17
31
|
this.$rect = $SVG('rect')
|
|
18
32
|
.attr('width', options.position.get('w'))
|
|
@@ -20,18 +34,113 @@ class Group extends BaseObject {
|
|
|
20
34
|
.css('fill', options.position.get('fill'))
|
|
21
35
|
.appendTo(this.$el)
|
|
22
36
|
|
|
37
|
+
this.$handle = $SVG('rect')
|
|
38
|
+
.attr('class', 'handle')
|
|
39
|
+
.attr('width', Group.HANDLE)
|
|
40
|
+
.attr('height', Group.HANDLE)
|
|
41
|
+
.appendTo(this.$el)
|
|
42
|
+
|
|
23
43
|
super.initialize(options)
|
|
44
|
+
|
|
45
|
+
this.listenTo(this.position, 'change:collapsed', this.UpdateVisibility)
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
render() {
|
|
49
|
+
let x = this.position.get('x')
|
|
50
|
+
let y = this.position.get('y')
|
|
51
|
+
let w = this.position.get('w')
|
|
52
|
+
let h = this.Height()
|
|
53
|
+
|
|
27
54
|
this.$rect
|
|
28
|
-
.attr('width',
|
|
29
|
-
.attr('height',
|
|
30
|
-
.attr('x',
|
|
31
|
-
.attr('y',
|
|
55
|
+
.attr('width', w)
|
|
56
|
+
.attr('height', h)
|
|
57
|
+
.attr('x', x)
|
|
58
|
+
.attr('y', y)
|
|
59
|
+
|
|
60
|
+
// no resizing while collapsed
|
|
61
|
+
this.$handle
|
|
62
|
+
.attr('x', x + w - Group.HANDLE)
|
|
63
|
+
.attr('y', y + h - Group.HANDLE)
|
|
64
|
+
.css('display', this.position.get('collapsed') ? 'none' : '')
|
|
65
|
+
|
|
32
66
|
return this
|
|
33
67
|
}
|
|
34
68
|
|
|
69
|
+
// drawn height; `h` keeps the expanded height while collapsed
|
|
70
|
+
Height() {
|
|
71
|
+
return this.position.get('collapsed') ? Group.COLLAPSED_HEIGHT : this.position.get('h')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Collapse() {
|
|
75
|
+
this.position.set({collapsed: true})
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Expand() {
|
|
80
|
+
this.position.set({collapsed: false})
|
|
81
|
+
return this
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ToggleCollapse() {
|
|
85
|
+
this.position.set({collapsed: !this.position.get('collapsed')})
|
|
86
|
+
return this
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// set the size, but no smaller than the group's contents, and grow the
|
|
90
|
+
// parent group if this one no longer fits inside it
|
|
91
|
+
Resize(w, h) {
|
|
92
|
+
let [minw, minh] = this.MinSize()
|
|
93
|
+
this.Size(Math.max(w, minw), Math.max(h, minh))
|
|
94
|
+
|
|
95
|
+
if (this.parent) {
|
|
96
|
+
let [pw, ph] = this.Extent()
|
|
97
|
+
this.parent.Resize(
|
|
98
|
+
Math.max(this.parent.position.get('w'), pw),
|
|
99
|
+
Math.max(this.parent.position.get('h'), ph))
|
|
100
|
+
}
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// the smallest size that still holds every child
|
|
105
|
+
MinSize() {
|
|
106
|
+
let w = Group.MIN_SIZE
|
|
107
|
+
let h = Group.MIN_SIZE
|
|
108
|
+
for (const child of this.children) {
|
|
109
|
+
let [cw, ch] = child.Extent()
|
|
110
|
+
w = Math.max(w, cw)
|
|
111
|
+
h = Math.max(h, ch)
|
|
112
|
+
}
|
|
113
|
+
return [w, h]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// the room this group needs inside its parent, keeping the 10px margin of Drag
|
|
117
|
+
Extent() {
|
|
118
|
+
return [
|
|
119
|
+
this.offset.x + this.position.get('w') + 10,
|
|
120
|
+
this.offset.y + this.Height() + 10,
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// dragging the handle resizes the group, through a drag item of its own so
|
|
125
|
+
// the group's click hooks don't fire for it
|
|
126
|
+
OnHandleDown(event) {
|
|
127
|
+
let start = {
|
|
128
|
+
x: event.clientX,
|
|
129
|
+
y: event.clientY,
|
|
130
|
+
w: this.position.get('w'),
|
|
131
|
+
h: this.position.get('h'),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
context.a.dragItem = {
|
|
135
|
+
jitter: 0,
|
|
136
|
+
Drag: (event) => this.Resize(
|
|
137
|
+
start.w + (event.clientX - start.x) / context.a.scale,
|
|
138
|
+
start.h + (event.clientY - start.y) / context.a.scale),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return cancelEvent(event)
|
|
142
|
+
}
|
|
143
|
+
|
|
35
144
|
OnMouseEnter(event) {
|
|
36
145
|
if (!context.a.dragItem) {
|
|
37
146
|
this.$rect.attr('class', 'hover')
|
|
@@ -58,7 +167,7 @@ class Group extends BaseObject {
|
|
|
58
167
|
this.offset.y = this.oy1 + y
|
|
59
168
|
|
|
60
169
|
let boundsx = this.parent.position.get('w') - this.position.get('w') - 10
|
|
61
|
-
let boundsy = this.parent.position.get('h') - this.
|
|
170
|
+
let boundsy = this.parent.position.get('h') - this.Height() - 10
|
|
62
171
|
|
|
63
172
|
if (this.offset.x < 10) {
|
|
64
173
|
this.offset.x = 10
|
package/js/Link.js
CHANGED
|
@@ -10,16 +10,22 @@ const context = require('./Context.js')
|
|
|
10
10
|
class Link extends Backbone.View {
|
|
11
11
|
static RAD = 180.0 / Math.PI
|
|
12
12
|
|
|
13
|
+
// render once the whole initialize chain has run, as BaseObject does
|
|
14
|
+
constructor(options) {
|
|
15
|
+
super(options)
|
|
16
|
+
this.render()
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
el() {
|
|
14
20
|
return $SVG('g').attr('class', 'link')
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
events() {
|
|
18
24
|
return {
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'contextmenu'
|
|
25
|
+
'pointerdown' : 'OnMouseDown',
|
|
26
|
+
'pointerenter' : 'OnMouseEnter',
|
|
27
|
+
'pointerleave' : 'OnMouseLeave',
|
|
28
|
+
'contextmenu' : 'OnContextMenu',
|
|
23
29
|
}
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -32,14 +38,19 @@ class Link extends Backbone.View {
|
|
|
32
38
|
_.bindAll(this, 'render', 'OnMouseDown', 'OnMouseEnter', 'OnMouseLeave', 'OnContextMenu')
|
|
33
39
|
|
|
34
40
|
this.arc = new Backbone.Model({arc:10})
|
|
35
|
-
this.arc
|
|
41
|
+
this.listenTo(this.arc, 'change', this.render)
|
|
36
42
|
|
|
37
43
|
this.left = options.left
|
|
38
44
|
this.right = options.right
|
|
39
|
-
this.left.position
|
|
40
|
-
this.right.position
|
|
45
|
+
this.listenTo(this.left.position, 'change', this.render)
|
|
46
|
+
this.listenTo(this.right.position, 'change', this.render)
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
// re-render when a group around either end collapses or expands
|
|
49
|
+
for (const end of [this.left, this.right]) {
|
|
50
|
+
for (let p = end.parent; p; p = p.parent) {
|
|
51
|
+
this.listenTo(p.position, 'change:collapsed', this.render)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
43
54
|
|
|
44
55
|
if (options.parent) {
|
|
45
56
|
options.parent.$links.append(this.$el)
|
|
@@ -51,14 +62,49 @@ class Link extends Backbone.View {
|
|
|
51
62
|
return
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
// the items the ends attach to: an end hidden inside a collapsed group
|
|
66
|
+
// attaches to that group instead
|
|
67
|
+
Ends() {
|
|
68
|
+
return [
|
|
69
|
+
this.left.CollapsedAncestor() || this.left,
|
|
70
|
+
this.right.CollapsedAncestor() || this.right,
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// the point a link aims at: a node's center, or the middle of a collapsed group
|
|
75
|
+
static Center(item) {
|
|
76
|
+
let p = item.position
|
|
77
|
+
if (p.get('collapsed')) {
|
|
78
|
+
return [p.get('x') + p.get('w') / 2, p.get('y') + item.Height() / 2]
|
|
79
|
+
}
|
|
80
|
+
return [p.get('x'), p.get('y')]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// distance from Center(item) to the item's edge along the unit vector (ux, uy)
|
|
84
|
+
static Edge(item, ux, uy) {
|
|
85
|
+
if (!item.position.get('collapsed')) {
|
|
86
|
+
return 30
|
|
87
|
+
}
|
|
88
|
+
return Math.min(
|
|
89
|
+
item.position.get('w') / 2 / Math.abs(ux),
|
|
90
|
+
item.Height() / 2 / Math.abs(uy))
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
render() {
|
|
55
94
|
if (!this.arc) { return this }
|
|
56
95
|
|
|
96
|
+
let [left, right] = this.Ends()
|
|
97
|
+
|
|
98
|
+
// both ends are hidden inside the same collapsed group
|
|
99
|
+
if (left === right) {
|
|
100
|
+
this.$el.css('display', 'none')
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
this.$el.css('display', '')
|
|
104
|
+
|
|
57
105
|
let arc = this.arc.get('arc')
|
|
58
|
-
let lx =
|
|
59
|
-
let
|
|
60
|
-
let rx = this.right.position.get('x')
|
|
61
|
-
let ry = this.right.position.get('y')
|
|
106
|
+
let [lx, ly] = Link.Center(left)
|
|
107
|
+
let [rx, ry] = Link.Center(right)
|
|
62
108
|
|
|
63
109
|
// calculate the angle between 2 nodes
|
|
64
110
|
let ang = Math.atan2(rx - lx, ry - ly)
|
|
@@ -66,11 +112,15 @@ class Link extends Backbone.View {
|
|
|
66
112
|
// bound the offset to about half the circle
|
|
67
113
|
let offset = Math.max(-1.5, Math.min(1.5, arc / 100))
|
|
68
114
|
|
|
69
|
-
// draw to the edge of the
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
115
|
+
// draw to the edge of each end, leaving room for the arrowhead
|
|
116
|
+
let la = ang + offset
|
|
117
|
+
let ra = ang - offset
|
|
118
|
+
let ld = Link.Edge(left, Math.sin(la), Math.cos(la))
|
|
119
|
+
let rd = Link.Edge(right, Math.sin(ra), Math.cos(ra)) + 3
|
|
120
|
+
lx += ld * Math.sin(la)
|
|
121
|
+
ly += ld * Math.cos(la)
|
|
122
|
+
rx -= rd * Math.sin(ra)
|
|
123
|
+
ry -= rd * Math.cos(ra)
|
|
74
124
|
|
|
75
125
|
// calculate the the position for the quadratic bezier curve
|
|
76
126
|
let xc = ((lx + rx) >> 1) + arc * Math.cos(ang)
|
|
@@ -79,11 +129,11 @@ class Link extends Backbone.View {
|
|
|
79
129
|
let mx = xc - (arc>>1) * Math.cos(ang)
|
|
80
130
|
let my = yc + (arc>>1) * Math.sin(ang)
|
|
81
131
|
|
|
82
|
-
let rot = -(
|
|
132
|
+
let rot = -(Link.RAD * ang)
|
|
83
133
|
rot += (rot > 0 && rot < 180) ? -90 : 90
|
|
84
134
|
|
|
85
|
-
this.path.attr('d',
|
|
86
|
-
//this.label.attr('x', mx).attr('y', my).attr('transform',
|
|
135
|
+
this.path.attr('d', `M ${lx} ${ly} Q ${xc} ${yc} ${rx} ${ry}`)
|
|
136
|
+
//this.label.attr('x', mx).attr('y', my).attr('transform', `rotate(${rot}, ${mx} ${my})`)
|
|
87
137
|
|
|
88
138
|
return this
|
|
89
139
|
}
|
|
@@ -93,15 +143,14 @@ class Link extends Backbone.View {
|
|
|
93
143
|
Drag(event) {
|
|
94
144
|
let [x,y] = context.a.Point(event)
|
|
95
145
|
|
|
96
|
-
let
|
|
97
|
-
let from_y =
|
|
98
|
-
let to_x
|
|
99
|
-
let to_y = this.right.position.get('y')
|
|
146
|
+
let [left, right] = this.Ends()
|
|
147
|
+
let [from_x, from_y] = Link.Center(left)
|
|
148
|
+
let [to_x, to_y] = Link.Center(right)
|
|
100
149
|
|
|
101
150
|
let d = (to_x - from_x) * (y - from_y) - (to_y - from_y) * (x - from_x)
|
|
102
151
|
|
|
103
152
|
if (d) {
|
|
104
|
-
d = Math.pow(Math.abs(d), 0.5) * (d > 0) ? -1 : 1
|
|
153
|
+
d = Math.pow(Math.abs(d), 0.5) * ((d > 0) ? -1 : 1)
|
|
105
154
|
}
|
|
106
155
|
|
|
107
156
|
if (!this.od && this.od != 0) {
|
|
@@ -123,7 +172,7 @@ class Link extends Backbone.View {
|
|
|
123
172
|
}
|
|
124
173
|
|
|
125
174
|
MouseUp(event) {
|
|
126
|
-
if (this.jitter
|
|
175
|
+
if (this.jitter >= 3) {
|
|
127
176
|
this.path.css('stroke-width', '3px')
|
|
128
177
|
}
|
|
129
178
|
}
|
package/js/Node.js
CHANGED
|
@@ -27,8 +27,6 @@ class Node extends BaseObject {
|
|
|
27
27
|
.appendTo(this.$el)
|
|
28
28
|
|
|
29
29
|
super.initialize(options)
|
|
30
|
-
|
|
31
|
-
this.render()
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
render() {
|
|
@@ -41,6 +39,12 @@ class Node extends BaseObject {
|
|
|
41
39
|
return this
|
|
42
40
|
}
|
|
43
41
|
|
|
42
|
+
// the whole circle, not just its center, belongs inside the parent group
|
|
43
|
+
Extent() {
|
|
44
|
+
let r = this.position.get('radius') || 0
|
|
45
|
+
return [this.offset.x + r, this.offset.y + r]
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
OnMouseEnter(event) {
|
|
45
49
|
if (!context.a.dragItem) {
|
|
46
50
|
this.$circle.attr('class', 'hover')
|
package/js/Socket.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
url : null, // full ws(s):// URL; overrides `path` when set
|
|
6
|
+
path : 'ws', // path appended to the current host
|
|
7
|
+
timeout : 0.9375, // seconds before the first reconnect attempt
|
|
8
|
+
maxtime : 60, // reconnect backoff is capped at this many seconds
|
|
9
|
+
ping : 300, // seconds between keepalive pings (0 disables)
|
|
10
|
+
onopen : function(event) {},
|
|
11
|
+
onclose : function(event) {},
|
|
12
|
+
onerror : function(error) {},
|
|
13
|
+
onmessage : function(msg) {},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Socket {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.options = Object.assign({}, DEFAULTS, options)
|
|
20
|
+
|
|
21
|
+
if (this.options.url) {
|
|
22
|
+
this.wsurl = this.options.url
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
let protocol = location.protocol.replace('http', 'ws')
|
|
26
|
+
this.wsurl = `${protocol}//${location.host}/${this.options.path}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.timeout = this.options.timeout
|
|
30
|
+
this.interval = null
|
|
31
|
+
this.retry = null
|
|
32
|
+
this.queue = []
|
|
33
|
+
this.closed = false
|
|
34
|
+
|
|
35
|
+
this.connect()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
connect() {
|
|
39
|
+
this.ws = new WebSocket(this.wsurl)
|
|
40
|
+
|
|
41
|
+
this.ws.onopen = (event) => {
|
|
42
|
+
// successful connection: reset the backoff and flush queued sends
|
|
43
|
+
this.timeout = this.options.timeout
|
|
44
|
+
|
|
45
|
+
for (const message of this.queue.splice(0)) {
|
|
46
|
+
this.ws.send(message)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.options.ping) {
|
|
50
|
+
this.interval = setInterval(() => {
|
|
51
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
52
|
+
this.ws.send('ping')
|
|
53
|
+
}
|
|
54
|
+
}, 1000 * this.options.ping)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.options.onopen(event)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.ws.onmessage = (event) => {
|
|
61
|
+
if (event.data === 'pong') {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let msg
|
|
66
|
+
try {
|
|
67
|
+
msg = JSON.parse(event.data)
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.options.onerror(error)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.options.onmessage(msg)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.ws.onerror = (event) => {
|
|
78
|
+
this.options.onerror(event)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.ws.onclose = (event) => {
|
|
82
|
+
clearInterval(this.interval)
|
|
83
|
+
this.interval = null
|
|
84
|
+
|
|
85
|
+
this.options.onclose(event)
|
|
86
|
+
|
|
87
|
+
// application-level signal to force a full page reload
|
|
88
|
+
if (event.code === 4004) {
|
|
89
|
+
window.location.reload()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// deliberate close() — do not reconnect
|
|
94
|
+
if (this.closed) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.retry = setTimeout(() => {
|
|
99
|
+
this.retry = null
|
|
100
|
+
this.connect()
|
|
101
|
+
}, 1000 * this.timeout)
|
|
102
|
+
|
|
103
|
+
// exponential backoff on every attempt, capped at maxtime
|
|
104
|
+
this.timeout = Math.min(this.timeout * 2, this.options.maxtime)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
send(message) {
|
|
109
|
+
this.sendraw(JSON.stringify(message))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
sendraw(message) {
|
|
113
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
114
|
+
this.ws.send(message)
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// queued until the (re)connection opens
|
|
118
|
+
this.queue.push(message)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
close() {
|
|
123
|
+
this.closed = true
|
|
124
|
+
clearInterval(this.interval)
|
|
125
|
+
clearTimeout(this.retry)
|
|
126
|
+
this.interval = null
|
|
127
|
+
this.retry = null
|
|
128
|
+
if (this.ws) {
|
|
129
|
+
this.ws.close()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = Socket
|
package/js/Types.js
CHANGED
package/js/Utils.js
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
const $ = require('jquery')
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
exports.cancelEvent = function(event) {
|
|
7
6
|
event.preventDefault()
|
|
8
7
|
event.stopPropagation()
|
|
9
8
|
return false
|
|
10
9
|
}
|
|
11
10
|
|
|
11
|
+
exports.$SVG = function(name) {
|
|
12
|
+
return $(document.createElementNS('http://www.w3.org/2000/svg', name))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// zoom steps for a `wheel` event: about 1 per mouse-wheel notch, positive when
|
|
16
|
+
// scrolling up. jQuery doesn't copy the deltas, so read the native event.
|
|
12
17
|
exports.normalizeWheel = function(event) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
event = event.originalEvent || event
|
|
19
|
+
switch (event.deltaMode) {
|
|
20
|
+
case 0: return -event.deltaY / 100 // pixels: ~100 per notch
|
|
21
|
+
case 1: return -event.deltaY / 3 // lines: 3 per notch
|
|
22
|
+
default: return -Math.sign(event.deltaY) // pages
|
|
23
|
+
}
|
|
16
24
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shawcx/avispa",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "SVG
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Draw node/link graphs in SVG and drag, pan, zoom, and collapse them; Backbone views with an optional WebSocket feed",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"svg",
|
|
7
|
+
"graph",
|
|
8
|
+
"network",
|
|
9
|
+
"diagram",
|
|
10
|
+
"backbone",
|
|
11
|
+
"websocket"
|
|
12
|
+
],
|
|
5
13
|
"main": "js/Avispa.js",
|
|
6
14
|
"sass": "scss/Avispa.scss",
|
|
7
15
|
"style": "css/Avispa.css",
|
|
16
|
+
"files": [
|
|
17
|
+
"js",
|
|
18
|
+
"css",
|
|
19
|
+
"scss"
|
|
20
|
+
],
|
|
8
21
|
"scripts": {
|
|
9
|
-
"build": "npx sass scss/Avispa.scss css/Avispa.css"
|
|
22
|
+
"build": "npx sass scss/Avispa.scss css/Avispa.css",
|
|
23
|
+
"example": "npm --prefix example install && npm --prefix example start",
|
|
24
|
+
"test": "playwright test",
|
|
25
|
+
"pretest": "npm --prefix example install --no-audit --no-fund && npm --prefix example run build",
|
|
26
|
+
"publish:skunk": "SKUNK_NPM_TOKEN=$(glab config get token --host code.skunk.tools) npm publish --@shawcx:registry=https://code.skunk.tools/api/v4/projects/69/packages/npm/"
|
|
10
27
|
},
|
|
11
28
|
"repository": {
|
|
12
29
|
"type": "git",
|
|
@@ -18,12 +35,16 @@
|
|
|
18
35
|
"url": "https://github.com/shawcx/avispa/issues"
|
|
19
36
|
},
|
|
20
37
|
"homepage": "https://github.com/shawcx/avispa#readme",
|
|
21
|
-
"
|
|
38
|
+
"peerDependencies": {
|
|
22
39
|
"backbone": "^1.4.1",
|
|
23
|
-
"jquery": "^3.6.3",
|
|
40
|
+
"jquery": "^3.6.3 || ^4.0.0",
|
|
24
41
|
"underscore": "^1.13.6"
|
|
25
42
|
},
|
|
26
43
|
"devDependencies": {
|
|
27
|
-
"
|
|
44
|
+
"@playwright/test": "^1.64.1",
|
|
45
|
+
"backbone": "^1.4.1",
|
|
46
|
+
"jquery": "^3.6.3 || ^4.0.0",
|
|
47
|
+
"sass": "^1.58.2",
|
|
48
|
+
"underscore": "^1.13.6"
|
|
28
49
|
}
|
|
29
50
|
}
|
package/scss/Avispa.scss
CHANGED
|
@@ -17,9 +17,15 @@ g.group {
|
|
|
17
17
|
stroke-width: 2px;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
rect.handle {
|
|
21
|
+
cursor: nwse-resize;
|
|
22
|
+
fill: #999;
|
|
23
|
+
stroke: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
text {
|
|
21
27
|
cursor: pointer;
|
|
22
|
-
font: bold
|
|
28
|
+
font: bold 10pt $mono;
|
|
23
29
|
fill: #000;
|
|
24
30
|
text-anchor: left;
|
|
25
31
|
}
|