@joint/core 4.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/LICENSE +376 -0
- package/README.md +49 -0
- package/dist/geometry.js +6486 -0
- package/dist/geometry.min.js +8 -0
- package/dist/joint.d.ts +5536 -0
- package/dist/joint.js +39629 -0
- package/dist/joint.min.js +8 -0
- package/dist/joint.nowrap.js +39626 -0
- package/dist/joint.nowrap.min.js +8 -0
- package/dist/vectorizer.js +9135 -0
- package/dist/vectorizer.min.js +8 -0
- package/dist/version.mjs +3 -0
- package/index.js +3 -0
- package/joint.mjs +27 -0
- package/package.json +192 -0
- package/src/V/annotation.mjs +0 -0
- package/src/V/index.mjs +2642 -0
- package/src/anchors/index.mjs +123 -0
- package/src/config/index.mjs +12 -0
- package/src/connectionPoints/index.mjs +202 -0
- package/src/connectionStrategies/index.mjs +73 -0
- package/src/connectors/curve.mjs +553 -0
- package/src/connectors/index.mjs +6 -0
- package/src/connectors/jumpover.mjs +452 -0
- package/src/connectors/normal.mjs +12 -0
- package/src/connectors/rounded.mjs +17 -0
- package/src/connectors/smooth.mjs +44 -0
- package/src/connectors/straight.mjs +110 -0
- package/src/dia/Cell.mjs +945 -0
- package/src/dia/CellView.mjs +1316 -0
- package/src/dia/Element.mjs +519 -0
- package/src/dia/ElementView.mjs +859 -0
- package/src/dia/Graph.mjs +1112 -0
- package/src/dia/HighlighterView.mjs +319 -0
- package/src/dia/Link.mjs +565 -0
- package/src/dia/LinkView.mjs +2207 -0
- package/src/dia/Paper.mjs +3171 -0
- package/src/dia/PaperLayer.mjs +75 -0
- package/src/dia/ToolView.mjs +69 -0
- package/src/dia/ToolsView.mjs +128 -0
- package/src/dia/attributes/calc.mjs +128 -0
- package/src/dia/attributes/connection.mjs +75 -0
- package/src/dia/attributes/defs.mjs +76 -0
- package/src/dia/attributes/eval.mjs +64 -0
- package/src/dia/attributes/index.mjs +69 -0
- package/src/dia/attributes/legacy.mjs +148 -0
- package/src/dia/attributes/offset.mjs +53 -0
- package/src/dia/attributes/props.mjs +30 -0
- package/src/dia/attributes/shape.mjs +92 -0
- package/src/dia/attributes/text.mjs +180 -0
- package/src/dia/index.mjs +13 -0
- package/src/dia/layers/GridLayer.mjs +176 -0
- package/src/dia/ports.mjs +874 -0
- package/src/elementTools/Control.mjs +153 -0
- package/src/elementTools/HoverConnect.mjs +37 -0
- package/src/elementTools/index.mjs +5 -0
- package/src/env/index.mjs +43 -0
- package/src/g/bezier.mjs +175 -0
- package/src/g/curve.mjs +956 -0
- package/src/g/ellipse.mjs +245 -0
- package/src/g/extend.mjs +64 -0
- package/src/g/geometry.helpers.mjs +58 -0
- package/src/g/index.mjs +17 -0
- package/src/g/intersection.mjs +511 -0
- package/src/g/line.bearing.mjs +30 -0
- package/src/g/line.length.mjs +5 -0
- package/src/g/line.mjs +356 -0
- package/src/g/line.squaredLength.mjs +10 -0
- package/src/g/path.mjs +2260 -0
- package/src/g/point.mjs +375 -0
- package/src/g/points.mjs +247 -0
- package/src/g/polygon.mjs +51 -0
- package/src/g/polyline.mjs +523 -0
- package/src/g/rect.mjs +556 -0
- package/src/g/types.mjs +10 -0
- package/src/highlighters/addClass.mjs +27 -0
- package/src/highlighters/index.mjs +5 -0
- package/src/highlighters/list.mjs +111 -0
- package/src/highlighters/mask.mjs +220 -0
- package/src/highlighters/opacity.mjs +17 -0
- package/src/highlighters/stroke.mjs +100 -0
- package/src/layout/index.mjs +4 -0
- package/src/layout/ports/port.mjs +188 -0
- package/src/layout/ports/portLabel.mjs +224 -0
- package/src/linkAnchors/index.mjs +76 -0
- package/src/linkTools/Anchor.mjs +235 -0
- package/src/linkTools/Arrowhead.mjs +103 -0
- package/src/linkTools/Boundary.mjs +48 -0
- package/src/linkTools/Button.mjs +121 -0
- package/src/linkTools/Connect.mjs +85 -0
- package/src/linkTools/HoverConnect.mjs +161 -0
- package/src/linkTools/Segments.mjs +393 -0
- package/src/linkTools/Vertices.mjs +253 -0
- package/src/linkTools/helpers.mjs +33 -0
- package/src/linkTools/index.mjs +8 -0
- package/src/mvc/Collection.mjs +560 -0
- package/src/mvc/Data.mjs +46 -0
- package/src/mvc/Dom/Dom.mjs +587 -0
- package/src/mvc/Dom/Event.mjs +130 -0
- package/src/mvc/Dom/animations.mjs +122 -0
- package/src/mvc/Dom/events.mjs +69 -0
- package/src/mvc/Dom/index.mjs +13 -0
- package/src/mvc/Dom/methods.mjs +392 -0
- package/src/mvc/Dom/props.mjs +77 -0
- package/src/mvc/Dom/vars.mjs +5 -0
- package/src/mvc/Events.mjs +337 -0
- package/src/mvc/Listener.mjs +33 -0
- package/src/mvc/Model.mjs +239 -0
- package/src/mvc/View.mjs +323 -0
- package/src/mvc/ViewBase.mjs +182 -0
- package/src/mvc/index.mjs +9 -0
- package/src/mvc/mvcUtils.mjs +90 -0
- package/src/polyfills/array.js +4 -0
- package/src/polyfills/base64.js +68 -0
- package/src/polyfills/index.mjs +5 -0
- package/src/polyfills/number.js +3 -0
- package/src/polyfills/string.js +3 -0
- package/src/polyfills/typedArray.js +47 -0
- package/src/routers/index.mjs +6 -0
- package/src/routers/manhattan.mjs +856 -0
- package/src/routers/metro.mjs +91 -0
- package/src/routers/normal.mjs +6 -0
- package/src/routers/oneSide.mjs +60 -0
- package/src/routers/orthogonal.mjs +323 -0
- package/src/routers/rightAngle.mjs +1056 -0
- package/src/shapes/index.mjs +3 -0
- package/src/shapes/standard.mjs +755 -0
- package/src/util/cloneCells.mjs +67 -0
- package/src/util/getRectPoint.mjs +65 -0
- package/src/util/index.mjs +5 -0
- package/src/util/svgTagTemplate.mjs +110 -0
- package/src/util/util.mjs +1754 -0
- package/src/util/utilHelpers.mjs +2402 -0
- package/src/util/wrappers.mjs +56 -0
- package/types/geometry.d.ts +815 -0
- package/types/index.d.ts +53 -0
- package/types/joint.d.ts +4391 -0
- package/types/joint.head.d.ts +12 -0
- package/types/vectorizer.d.ts +327 -0
|
@@ -0,0 +1,3171 @@
|
|
|
1
|
+
import V from '../V/index.mjs';
|
|
2
|
+
import {
|
|
3
|
+
isNumber,
|
|
4
|
+
assign,
|
|
5
|
+
nextFrame,
|
|
6
|
+
isObject,
|
|
7
|
+
cancelFrame,
|
|
8
|
+
defaults,
|
|
9
|
+
defaultsDeep,
|
|
10
|
+
addClassNamePrefix,
|
|
11
|
+
normalizeSides,
|
|
12
|
+
isFunction,
|
|
13
|
+
isPlainObject,
|
|
14
|
+
getByPath,
|
|
15
|
+
sortElements,
|
|
16
|
+
isString,
|
|
17
|
+
guid,
|
|
18
|
+
normalizeEvent,
|
|
19
|
+
normalizeWheel,
|
|
20
|
+
cap,
|
|
21
|
+
debounce,
|
|
22
|
+
omit,
|
|
23
|
+
result,
|
|
24
|
+
camelCase,
|
|
25
|
+
cloneDeep,
|
|
26
|
+
invoke,
|
|
27
|
+
hashCode,
|
|
28
|
+
filter as _filter,
|
|
29
|
+
parseDOMJSON,
|
|
30
|
+
toArray,
|
|
31
|
+
has
|
|
32
|
+
} from '../util/index.mjs';
|
|
33
|
+
import { ViewBase } from '../mvc/ViewBase.mjs';
|
|
34
|
+
import { Rect, Point, toRad } from '../g/index.mjs';
|
|
35
|
+
import { View, views } from '../mvc/index.mjs';
|
|
36
|
+
import { CellView } from './CellView.mjs';
|
|
37
|
+
import { ElementView } from './ElementView.mjs';
|
|
38
|
+
import { LinkView } from './LinkView.mjs';
|
|
39
|
+
import { Cell } from './Cell.mjs';
|
|
40
|
+
import { Graph } from './Graph.mjs';
|
|
41
|
+
import { LayersNames, PaperLayer } from './PaperLayer.mjs';
|
|
42
|
+
import * as highlighters from '../highlighters/index.mjs';
|
|
43
|
+
import * as linkAnchors from '../linkAnchors/index.mjs';
|
|
44
|
+
import * as connectionPoints from '../connectionPoints/index.mjs';
|
|
45
|
+
import * as anchors from '../anchors/index.mjs';
|
|
46
|
+
|
|
47
|
+
import $ from '../mvc/Dom/index.mjs';
|
|
48
|
+
import { GridLayer } from './layers/GridLayer.mjs';
|
|
49
|
+
|
|
50
|
+
const sortingTypes = {
|
|
51
|
+
NONE: 'sorting-none',
|
|
52
|
+
APPROX: 'sorting-approximate',
|
|
53
|
+
EXACT: 'sorting-exact'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const WHEEL_CAP = 50;
|
|
57
|
+
const WHEEL_WAIT_MS = 20;
|
|
58
|
+
const MOUNT_BATCH_SIZE = 1000;
|
|
59
|
+
const UPDATE_BATCH_SIZE = Infinity;
|
|
60
|
+
const MIN_PRIORITY = 9007199254740991; // Number.MAX_SAFE_INTEGER
|
|
61
|
+
|
|
62
|
+
const HighlightingTypes = CellView.Highlighting;
|
|
63
|
+
|
|
64
|
+
const defaultHighlighting = {
|
|
65
|
+
[HighlightingTypes.DEFAULT]: {
|
|
66
|
+
name: 'stroke',
|
|
67
|
+
options: {
|
|
68
|
+
padding: 3
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[HighlightingTypes.MAGNET_AVAILABILITY]: {
|
|
72
|
+
name: 'addClass',
|
|
73
|
+
options: {
|
|
74
|
+
className: 'available-magnet'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[HighlightingTypes.ELEMENT_AVAILABILITY]: {
|
|
78
|
+
name: 'addClass',
|
|
79
|
+
options: {
|
|
80
|
+
className: 'available-cell'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const defaultLayers = [{
|
|
86
|
+
name: LayersNames.GRID,
|
|
87
|
+
}, {
|
|
88
|
+
name: LayersNames.BACK,
|
|
89
|
+
}, {
|
|
90
|
+
name: LayersNames.CELLS,
|
|
91
|
+
}, {
|
|
92
|
+
name: LayersNames.LABELS,
|
|
93
|
+
}, {
|
|
94
|
+
name: LayersNames.FRONT
|
|
95
|
+
}, {
|
|
96
|
+
name: LayersNames.TOOLS
|
|
97
|
+
}];
|
|
98
|
+
|
|
99
|
+
export const Paper = View.extend({
|
|
100
|
+
|
|
101
|
+
className: 'paper',
|
|
102
|
+
|
|
103
|
+
options: {
|
|
104
|
+
|
|
105
|
+
width: 800,
|
|
106
|
+
height: 600,
|
|
107
|
+
gridSize: 1,
|
|
108
|
+
// Whether or not to draw the grid lines on the paper's DOM element.
|
|
109
|
+
// e.g drawGrid: true, drawGrid: { color: 'red', thickness: 2 }
|
|
110
|
+
drawGrid: false,
|
|
111
|
+
// If not set, the size of the visual grid is the same as the `gridSize`.
|
|
112
|
+
drawGridSize: null,
|
|
113
|
+
|
|
114
|
+
// Whether or not to draw the background on the paper's DOM element.
|
|
115
|
+
// e.g. background: { color: 'lightblue', image: '/paper-background.png', repeat: 'flip-xy' }
|
|
116
|
+
background: false,
|
|
117
|
+
|
|
118
|
+
elementView: ElementView,
|
|
119
|
+
linkView: LinkView,
|
|
120
|
+
snapLabels: false, // false, true
|
|
121
|
+
snapLinks: false, // false, true, { radius: value }
|
|
122
|
+
snapLinksSelf: false, // false, true, { radius: value }
|
|
123
|
+
|
|
124
|
+
// Should the link labels be rendered into its own layer?
|
|
125
|
+
// `false` - the labels are part of the links
|
|
126
|
+
// `true` - the labels are appended to LayersName.LABELS
|
|
127
|
+
// [LayersName] - the labels are appended to the layer specified
|
|
128
|
+
labelsLayer: false,
|
|
129
|
+
|
|
130
|
+
// When set to FALSE, an element may not have more than 1 link with the same source and target element.
|
|
131
|
+
multiLinks: true,
|
|
132
|
+
|
|
133
|
+
// For adding custom guard logic.
|
|
134
|
+
guard: function(evt, view) {
|
|
135
|
+
|
|
136
|
+
// FALSE means the event isn't guarded.
|
|
137
|
+
return false;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
highlighting: defaultHighlighting,
|
|
141
|
+
|
|
142
|
+
// Prevent the default context menu from being displayed.
|
|
143
|
+
preventContextMenu: true,
|
|
144
|
+
|
|
145
|
+
// Prevent the default action for blank:pointer<action>.
|
|
146
|
+
preventDefaultBlankAction: true,
|
|
147
|
+
|
|
148
|
+
// Prevent the default action for cell:pointer<action>.
|
|
149
|
+
preventDefaultViewAction: true,
|
|
150
|
+
|
|
151
|
+
// Restrict the translation of elements by given bounding box.
|
|
152
|
+
// Option accepts a boolean:
|
|
153
|
+
// true - the translation is restricted to the paper area
|
|
154
|
+
// false - no restrictions
|
|
155
|
+
// A method:
|
|
156
|
+
// restrictTranslate: function(elementView) {
|
|
157
|
+
// var parentId = elementView.model.get('parent');
|
|
158
|
+
// return parentId && this.model.getCell(parentId).getBBox();
|
|
159
|
+
// },
|
|
160
|
+
// Or a bounding box:
|
|
161
|
+
// restrictTranslate: { x: 10, y: 10, width: 790, height: 590 }
|
|
162
|
+
restrictTranslate: false,
|
|
163
|
+
|
|
164
|
+
// Marks all available magnets with 'available-magnet' class name and all available cells with
|
|
165
|
+
// 'available-cell' class name. Marks them when dragging a link is started and unmark
|
|
166
|
+
// when the dragging is stopped.
|
|
167
|
+
markAvailable: false,
|
|
168
|
+
|
|
169
|
+
// Defines what link model is added to the graph after an user clicks on an active magnet.
|
|
170
|
+
// Value could be the mvc.model or a function returning the mvc.model
|
|
171
|
+
// defaultLink: (elementView, magnet) => {
|
|
172
|
+
// return condition ? new customLink1() : new customLink2()
|
|
173
|
+
// }
|
|
174
|
+
defaultLink: function() {
|
|
175
|
+
// Do not create hard dependency on the joint.shapes.standard namespace (by importing the standard.Link model directly)
|
|
176
|
+
const { cellNamespace } = this.model.get('cells');
|
|
177
|
+
const ctor = getByPath(cellNamespace, ['standard', 'Link']);
|
|
178
|
+
if (!ctor) throw new Error('dia.Paper: no default link model found. Use `options.defaultLink` to specify a default link model.');
|
|
179
|
+
return new ctor();
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// A connector that is used by links with no connector defined on the model.
|
|
183
|
+
// e.g. { name: 'rounded', args: { radius: 5 }} or a function
|
|
184
|
+
defaultConnector: { name: 'normal' },
|
|
185
|
+
|
|
186
|
+
// A router that is used by links with no router defined on the model.
|
|
187
|
+
// e.g. { name: 'oneSide', args: { padding: 10 }} or a function
|
|
188
|
+
defaultRouter: { name: 'normal' },
|
|
189
|
+
|
|
190
|
+
defaultAnchor: { name: 'center' },
|
|
191
|
+
|
|
192
|
+
defaultLinkAnchor: { name: 'connectionRatio' },
|
|
193
|
+
|
|
194
|
+
defaultConnectionPoint: { name: 'boundary' },
|
|
195
|
+
|
|
196
|
+
/* CONNECTING */
|
|
197
|
+
|
|
198
|
+
connectionStrategy: null,
|
|
199
|
+
|
|
200
|
+
// Check whether to add a new link to the graph when user clicks on an a magnet.
|
|
201
|
+
validateMagnet: function(_cellView, magnet, _evt) {
|
|
202
|
+
return magnet.getAttribute('magnet') !== 'passive';
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// Check whether to allow or disallow the link connection while an arrowhead end (source/target)
|
|
206
|
+
// being changed.
|
|
207
|
+
validateConnection: function(cellViewS, _magnetS, cellViewT, _magnetT, end, _linkView) {
|
|
208
|
+
return (end === 'target' ? cellViewT : cellViewS) instanceof ElementView;
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/* EMBEDDING */
|
|
212
|
+
|
|
213
|
+
// Enables embedding. Re-parent the dragged element with elements under it and makes sure that
|
|
214
|
+
// all links and elements are visible taken the level of embedding into account.
|
|
215
|
+
embeddingMode: false,
|
|
216
|
+
|
|
217
|
+
// Check whether to allow or disallow the element embedding while an element being translated.
|
|
218
|
+
validateEmbedding: function(childView, parentView) {
|
|
219
|
+
// by default all elements can be in relation child-parent
|
|
220
|
+
return true;
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// Check whether to allow or disallow an embedded element to be unembedded / to become a root.
|
|
224
|
+
validateUnembedding: function(childView) {
|
|
225
|
+
// by default all elements can become roots
|
|
226
|
+
return true;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Determines the way how a cell finds a suitable parent when it's dragged over the paper.
|
|
230
|
+
// The cell with the highest z-index (visually on the top) will be chosen.
|
|
231
|
+
findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft'
|
|
232
|
+
|
|
233
|
+
// If enabled only the element on the very front is taken into account for the embedding.
|
|
234
|
+
// If disabled the elements under the dragged view are tested one by one
|
|
235
|
+
// (from front to back) until a valid parent found.
|
|
236
|
+
frontParentOnly: true,
|
|
237
|
+
|
|
238
|
+
// Interactive flags. See online docs for the complete list of interactive flags.
|
|
239
|
+
interactive: {
|
|
240
|
+
labelMove: false
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// When set to true the links can be pinned to the paper.
|
|
244
|
+
// i.e. link source/target can be a point e.g. link.get('source') ==> { x: 100, y: 100 };
|
|
245
|
+
linkPinning: true,
|
|
246
|
+
|
|
247
|
+
// Custom validation after an interaction with a link ends.
|
|
248
|
+
// Recognizes a function. If `false` is returned, the link is disallowed (removed or reverted)
|
|
249
|
+
// (linkView, paper) => boolean
|
|
250
|
+
allowLink: null,
|
|
251
|
+
|
|
252
|
+
// Allowed number of mousemove events after which the pointerclick event will be still triggered.
|
|
253
|
+
clickThreshold: 0,
|
|
254
|
+
|
|
255
|
+
// Number of required mousemove events before the first pointermove event will be triggered.
|
|
256
|
+
moveThreshold: 0,
|
|
257
|
+
|
|
258
|
+
// Number of required mousemove events before a link is created out of the magnet.
|
|
259
|
+
// Or string `onleave` so the link is created when the pointer leaves the magnet
|
|
260
|
+
magnetThreshold: 0,
|
|
261
|
+
|
|
262
|
+
// Rendering Options
|
|
263
|
+
|
|
264
|
+
sorting: sortingTypes.APPROX,
|
|
265
|
+
|
|
266
|
+
frozen: false,
|
|
267
|
+
|
|
268
|
+
autoFreeze: false,
|
|
269
|
+
|
|
270
|
+
// no docs yet
|
|
271
|
+
onViewUpdate: function(view, flag, priority, opt, paper) {
|
|
272
|
+
// Do not update connected links when:
|
|
273
|
+
// 1. the view was just inserted (added to the graph and rendered)
|
|
274
|
+
// 2. the view was just mounted (added back to the paper by viewport function)
|
|
275
|
+
// 3. the change was marked as `isolate`.
|
|
276
|
+
// 4. the view model was just removed from the graph
|
|
277
|
+
if ((flag & (view.FLAG_INSERT | view.FLAG_REMOVE)) || opt.mounting || opt.isolate) return;
|
|
278
|
+
paper.requestConnectedLinksUpdate(view, priority, opt);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// no docs yet
|
|
282
|
+
onViewPostponed: function(view, flag, paper) {
|
|
283
|
+
return paper.forcePostponedViewUpdate(view, flag);
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
beforeRender: null, // function(opt, paper) { },
|
|
287
|
+
|
|
288
|
+
afterRender: null, // function(stats, opt, paper) {
|
|
289
|
+
|
|
290
|
+
viewport: null,
|
|
291
|
+
|
|
292
|
+
// Default namespaces
|
|
293
|
+
|
|
294
|
+
cellViewNamespace: null,
|
|
295
|
+
|
|
296
|
+
routerNamespace: null,
|
|
297
|
+
|
|
298
|
+
connectorNamespace: null,
|
|
299
|
+
|
|
300
|
+
highlighterNamespace: highlighters,
|
|
301
|
+
|
|
302
|
+
anchorNamespace: anchors,
|
|
303
|
+
|
|
304
|
+
linkAnchorNamespace: linkAnchors,
|
|
305
|
+
|
|
306
|
+
connectionPointNamespace: connectionPoints,
|
|
307
|
+
|
|
308
|
+
overflow: false
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
events: {
|
|
312
|
+
'dblclick': 'pointerdblclick',
|
|
313
|
+
'dbltap': 'pointerdblclick',
|
|
314
|
+
'contextmenu': 'contextmenu',
|
|
315
|
+
'mousedown': 'pointerdown',
|
|
316
|
+
'touchstart': 'pointerdown',
|
|
317
|
+
'mouseover': 'mouseover',
|
|
318
|
+
'mouseout': 'mouseout',
|
|
319
|
+
'mouseenter': 'mouseenter',
|
|
320
|
+
'mouseleave': 'mouseleave',
|
|
321
|
+
'wheel': 'mousewheel',
|
|
322
|
+
'mouseenter .joint-cell': 'mouseenter',
|
|
323
|
+
'mouseleave .joint-cell': 'mouseleave',
|
|
324
|
+
'mouseenter .joint-tools': 'mouseenter',
|
|
325
|
+
'mouseleave .joint-tools': 'mouseleave',
|
|
326
|
+
'dblclick .joint-cell [magnet]': 'magnetpointerdblclick',
|
|
327
|
+
'contextmenu .joint-cell [magnet]': 'magnetcontextmenu',
|
|
328
|
+
'mousedown .joint-link .label': 'onlabel', // interaction with link label
|
|
329
|
+
'touchstart .joint-link .label': 'onlabel',
|
|
330
|
+
'dragstart .joint-cell image': 'onImageDragStart' // firefox fix
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
documentEvents: {
|
|
334
|
+
'mousemove': 'pointermove',
|
|
335
|
+
'touchmove': 'pointermove',
|
|
336
|
+
'mouseup': 'pointerup',
|
|
337
|
+
'touchend': 'pointerup',
|
|
338
|
+
'touchcancel': 'pointerup'
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
/* CSS within the SVG document
|
|
342
|
+
* 1. Adding vector-effect: non-scaling-stroke; to prevent the stroke width from scaling for
|
|
343
|
+
* elements that use the `scalable` group.
|
|
344
|
+
*/
|
|
345
|
+
stylesheet: /*css*/`
|
|
346
|
+
.joint-element .scalable * {
|
|
347
|
+
vector-effect: non-scaling-stroke;
|
|
348
|
+
}
|
|
349
|
+
`,
|
|
350
|
+
|
|
351
|
+
svg: null,
|
|
352
|
+
viewport: null,
|
|
353
|
+
defs: null,
|
|
354
|
+
tools: null,
|
|
355
|
+
layers: null,
|
|
356
|
+
|
|
357
|
+
// For storing the current transformation matrix (CTM) of the paper's viewport.
|
|
358
|
+
_viewportMatrix: null,
|
|
359
|
+
// For verifying whether the CTM is up-to-date. The viewport transform attribute
|
|
360
|
+
// could have been manipulated directly.
|
|
361
|
+
_viewportTransformString: null,
|
|
362
|
+
// Updates data (priorities, unmounted views etc.)
|
|
363
|
+
_updates: null,
|
|
364
|
+
// Paper Layers
|
|
365
|
+
_layers: null,
|
|
366
|
+
|
|
367
|
+
SORT_DELAYING_BATCHES: ['add', 'to-front', 'to-back'],
|
|
368
|
+
UPDATE_DELAYING_BATCHES: ['translate'],
|
|
369
|
+
// If you interact with these elements,
|
|
370
|
+
// the default interaction such as `element move` is prevented.
|
|
371
|
+
FORM_CONTROL_TAG_NAMES: ['TEXTAREA', 'INPUT', 'BUTTON', 'SELECT', 'OPTION'] ,
|
|
372
|
+
// If you interact with these elements, the events are not propagated to the paper
|
|
373
|
+
// i.e. paper events such as `element:pointerdown` are not triggered.
|
|
374
|
+
GUARDED_TAG_NAMES: [
|
|
375
|
+
// Guard <select> for consistency. When you click on it:
|
|
376
|
+
// Chrome: triggers `pointerdown`, `pointerup`, `pointerclick` to open
|
|
377
|
+
// Firefox: triggers `pointerdown` on open, `pointerup` (and `pointerclick` only if you haven't moved).
|
|
378
|
+
// on close. However, if you open and then close by clicking elsewhere on the page,
|
|
379
|
+
// no other event is triggered.
|
|
380
|
+
// Safari: when you open it, it triggers `pointerdown`. That's it.
|
|
381
|
+
'SELECT',
|
|
382
|
+
],
|
|
383
|
+
MIN_SCALE: 1e-6,
|
|
384
|
+
|
|
385
|
+
init: function() {
|
|
386
|
+
|
|
387
|
+
const { options } = this;
|
|
388
|
+
if (!options.cellViewNamespace) {
|
|
389
|
+
/* eslint-disable no-undef */
|
|
390
|
+
options.cellViewNamespace = typeof joint !== 'undefined' && has(joint, 'shapes') ? joint.shapes : null;
|
|
391
|
+
/* eslint-enable no-undef */
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const model = this.model = options.model || new Graph;
|
|
395
|
+
|
|
396
|
+
// Layers (SVGGroups)
|
|
397
|
+
this._layers = {};
|
|
398
|
+
|
|
399
|
+
this.cloneOptions();
|
|
400
|
+
this.render();
|
|
401
|
+
this._setDimensions();
|
|
402
|
+
this.startListening();
|
|
403
|
+
|
|
404
|
+
// Hash of all cell views.
|
|
405
|
+
this._views = {};
|
|
406
|
+
|
|
407
|
+
// Mouse wheel events buffer
|
|
408
|
+
this._mw_evt_buffer = {
|
|
409
|
+
event: null,
|
|
410
|
+
deltas: [],
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Render existing cells in the graph
|
|
414
|
+
this.resetViews(model.attributes.cells.models);
|
|
415
|
+
// Start the Rendering Loop
|
|
416
|
+
if (!this.isFrozen() && this.isAsync()) this.updateViewsAsync();
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
_resetUpdates: function() {
|
|
420
|
+
return this._updates = {
|
|
421
|
+
id: null,
|
|
422
|
+
priorities: [{}, {}, {}],
|
|
423
|
+
unmountedCids: [],
|
|
424
|
+
mountedCids: [],
|
|
425
|
+
unmounted: {},
|
|
426
|
+
mounted: {},
|
|
427
|
+
count: 0,
|
|
428
|
+
keyFrozen: false,
|
|
429
|
+
freezeKey: null,
|
|
430
|
+
sort: false,
|
|
431
|
+
disabled: false,
|
|
432
|
+
idle: false
|
|
433
|
+
};
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
startListening: function() {
|
|
437
|
+
var model = this.model;
|
|
438
|
+
this.listenTo(model, 'add', this.onCellAdded)
|
|
439
|
+
.listenTo(model, 'remove', this.onCellRemoved)
|
|
440
|
+
.listenTo(model, 'change', this.onCellChange)
|
|
441
|
+
.listenTo(model, 'reset', this.onGraphReset)
|
|
442
|
+
.listenTo(model, 'sort', this.onGraphSort)
|
|
443
|
+
.listenTo(model, 'batch:stop', this.onGraphBatchStop);
|
|
444
|
+
this.on('cell:highlight', this.onCellHighlight)
|
|
445
|
+
.on('cell:unhighlight', this.onCellUnhighlight)
|
|
446
|
+
.on('transform', this.update);
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
onCellAdded: function(cell, _, opt) {
|
|
450
|
+
var position = opt.position;
|
|
451
|
+
if (this.isAsync() || !isNumber(position)) {
|
|
452
|
+
this.renderView(cell, opt);
|
|
453
|
+
} else {
|
|
454
|
+
if (opt.maxPosition === position) this.freeze({ key: 'addCells' });
|
|
455
|
+
this.renderView(cell, opt);
|
|
456
|
+
if (position === 0) this.unfreeze({ key: 'addCells' });
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
onCellRemoved: function(cell, _, opt) {
|
|
461
|
+
const view = this.findViewByModel(cell);
|
|
462
|
+
if (view) this.requestViewUpdate(view, view.FLAG_REMOVE, view.UPDATE_PRIORITY, opt);
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
onCellChange: function(cell, opt) {
|
|
466
|
+
if (cell === this.model.attributes.cells) return;
|
|
467
|
+
if (cell.hasChanged('z') && this.options.sorting === sortingTypes.APPROX) {
|
|
468
|
+
const view = this.findViewByModel(cell);
|
|
469
|
+
if (view) this.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
onGraphReset: function(collection, opt) {
|
|
474
|
+
this.resetLayers();
|
|
475
|
+
this.resetViews(collection.models, opt);
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
onGraphSort: function() {
|
|
479
|
+
if (this.model.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return;
|
|
480
|
+
this.sortViews();
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
onGraphBatchStop: function(data) {
|
|
484
|
+
if (this.isFrozen()) return;
|
|
485
|
+
var name = data && data.batchName;
|
|
486
|
+
var graph = this.model;
|
|
487
|
+
if (!this.isAsync()) {
|
|
488
|
+
var updateDelayingBatches = this.UPDATE_DELAYING_BATCHES;
|
|
489
|
+
if (updateDelayingBatches.includes(name) && !graph.hasActiveBatch(updateDelayingBatches)) {
|
|
490
|
+
this.updateViews(data);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
var sortDelayingBatches = this.SORT_DELAYING_BATCHES;
|
|
494
|
+
if (sortDelayingBatches.includes(name) && !graph.hasActiveBatch(sortDelayingBatches)) {
|
|
495
|
+
this.sortViews();
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
|
|
499
|
+
cloneOptions: function() {
|
|
500
|
+
|
|
501
|
+
const { options } = this;
|
|
502
|
+
const {
|
|
503
|
+
defaultConnector,
|
|
504
|
+
defaultRouter,
|
|
505
|
+
defaultConnectionPoint,
|
|
506
|
+
defaultAnchor,
|
|
507
|
+
defaultLinkAnchor,
|
|
508
|
+
highlighting,
|
|
509
|
+
cellViewNamespace,
|
|
510
|
+
interactive
|
|
511
|
+
} = options;
|
|
512
|
+
|
|
513
|
+
// Default cellView namespace for ES5
|
|
514
|
+
/* eslint-disable no-undef */
|
|
515
|
+
if (!cellViewNamespace && typeof joint !== 'undefined' && has(joint, 'shapes')) {
|
|
516
|
+
options.cellViewNamespace = joint.shapes;
|
|
517
|
+
}
|
|
518
|
+
/* eslint-enable no-undef */
|
|
519
|
+
|
|
520
|
+
// Here if a function was provided, we can not clone it, as this would result in loosing the function.
|
|
521
|
+
// If the default is used, the cloning is necessary in order to prevent modifying the options on prototype.
|
|
522
|
+
if (!isFunction(defaultConnector)) {
|
|
523
|
+
options.defaultConnector = cloneDeep(defaultConnector);
|
|
524
|
+
}
|
|
525
|
+
if (!isFunction(defaultRouter)) {
|
|
526
|
+
options.defaultRouter = cloneDeep(defaultRouter);
|
|
527
|
+
}
|
|
528
|
+
if (!isFunction(defaultConnectionPoint)) {
|
|
529
|
+
options.defaultConnectionPoint = cloneDeep(defaultConnectionPoint);
|
|
530
|
+
}
|
|
531
|
+
if (!isFunction(defaultAnchor)) {
|
|
532
|
+
options.defaultAnchor = cloneDeep(defaultAnchor);
|
|
533
|
+
}
|
|
534
|
+
if (!isFunction(defaultLinkAnchor)) {
|
|
535
|
+
options.defaultLinkAnchor = cloneDeep(defaultLinkAnchor);
|
|
536
|
+
}
|
|
537
|
+
if (isPlainObject(interactive)) {
|
|
538
|
+
options.interactive = assign({}, interactive);
|
|
539
|
+
}
|
|
540
|
+
if (isPlainObject(highlighting)) {
|
|
541
|
+
// Return the default highlighting options into the user specified options.
|
|
542
|
+
options.highlighting = defaultsDeep({}, highlighting, defaultHighlighting);
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
children: function() {
|
|
547
|
+
var ns = V.namespace;
|
|
548
|
+
return [{
|
|
549
|
+
namespaceURI: ns.xhtml,
|
|
550
|
+
tagName: 'div',
|
|
551
|
+
className: addClassNamePrefix('paper-background'),
|
|
552
|
+
selector: 'background',
|
|
553
|
+
style: {
|
|
554
|
+
position: 'absolute',
|
|
555
|
+
inset: 0
|
|
556
|
+
}
|
|
557
|
+
}, {
|
|
558
|
+
namespaceURI: ns.svg,
|
|
559
|
+
tagName: 'svg',
|
|
560
|
+
attributes: {
|
|
561
|
+
'width': '100%',
|
|
562
|
+
'height': '100%',
|
|
563
|
+
'xmlns:xlink': ns.xlink
|
|
564
|
+
},
|
|
565
|
+
selector: 'svg',
|
|
566
|
+
style: {
|
|
567
|
+
position: 'absolute',
|
|
568
|
+
inset: 0
|
|
569
|
+
},
|
|
570
|
+
children: [{
|
|
571
|
+
// Append `<defs>` element to the SVG document. This is useful for filters and gradients.
|
|
572
|
+
// It's desired to have the defs defined before the viewport (e.g. to make a PDF document pick up defs properly).
|
|
573
|
+
tagName: 'defs',
|
|
574
|
+
selector: 'defs'
|
|
575
|
+
}, {
|
|
576
|
+
tagName: 'g',
|
|
577
|
+
className: addClassNamePrefix('layers'),
|
|
578
|
+
selector: 'layers'
|
|
579
|
+
}]
|
|
580
|
+
}];
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
hasLayerView(layerName) {
|
|
584
|
+
return (layerName in this._layers);
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
getLayerView(layerName) {
|
|
588
|
+
const { _layers } = this;
|
|
589
|
+
if (layerName in _layers) return _layers[layerName];
|
|
590
|
+
throw new Error(`dia.Paper: Unknown layer "${layerName}"`);
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
getLayerNode(layerName) {
|
|
594
|
+
return this.getLayerView(layerName).el;
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
render: function() {
|
|
598
|
+
|
|
599
|
+
this.renderChildren();
|
|
600
|
+
const { el, childNodes, options, stylesheet } = this;
|
|
601
|
+
const { svg, defs, layers } = childNodes;
|
|
602
|
+
|
|
603
|
+
el.style.position = 'relative';
|
|
604
|
+
svg.style.overflow = options.overflow ? 'visible' : 'hidden';
|
|
605
|
+
|
|
606
|
+
this.svg = svg;
|
|
607
|
+
this.defs = defs;
|
|
608
|
+
this.layers = layers;
|
|
609
|
+
|
|
610
|
+
this.renderLayers();
|
|
611
|
+
|
|
612
|
+
V.ensureId(svg);
|
|
613
|
+
|
|
614
|
+
this.addStylesheet(stylesheet);
|
|
615
|
+
|
|
616
|
+
if (options.background) {
|
|
617
|
+
this.drawBackground(options.background);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (options.drawGrid) {
|
|
621
|
+
this.setGrid(options.drawGrid);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return this;
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
addStylesheet: function(css) {
|
|
628
|
+
if (!css) return;
|
|
629
|
+
V(this.svg).prepend(V.createSVGStyle(css));
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
createLayer(name) {
|
|
633
|
+
switch (name) {
|
|
634
|
+
case LayersNames.GRID:
|
|
635
|
+
return new GridLayer({ name, paper: this, patterns: this.constructor.gridPatterns });
|
|
636
|
+
default:
|
|
637
|
+
return new PaperLayer({ name });
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
renderLayers: function(layers = defaultLayers) {
|
|
642
|
+
this.removeLayers();
|
|
643
|
+
// TODO: Layers to be read from the graph `layers` attribute
|
|
644
|
+
layers.forEach(({ name, sorted }) => {
|
|
645
|
+
const layerView = this.createLayer(name);
|
|
646
|
+
this.layers.appendChild(layerView.el);
|
|
647
|
+
this._layers[name] = layerView;
|
|
648
|
+
});
|
|
649
|
+
// Throws an exception if doesn't exist
|
|
650
|
+
const cellsLayerView = this.getLayerView(LayersNames.CELLS);
|
|
651
|
+
const toolsLayerView = this.getLayerView(LayersNames.TOOLS);
|
|
652
|
+
const labelsLayerView = this.getLayerView(LayersNames.LABELS);
|
|
653
|
+
// backwards compatibility
|
|
654
|
+
this.tools = toolsLayerView.el;
|
|
655
|
+
this.cells = this.viewport = cellsLayerView.el;
|
|
656
|
+
// user-select: none;
|
|
657
|
+
cellsLayerView.vel.addClass(addClassNamePrefix('viewport'));
|
|
658
|
+
labelsLayerView.vel.addClass(addClassNamePrefix('viewport'));
|
|
659
|
+
cellsLayerView.el.style.webkitUserSelect = 'none';
|
|
660
|
+
cellsLayerView.el.style.userSelect = 'none';
|
|
661
|
+
labelsLayerView.el.style.webkitUserSelect = 'none';
|
|
662
|
+
labelsLayerView.el.style.userSelect = 'none';
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
removeLayers: function() {
|
|
666
|
+
const { _layers } = this;
|
|
667
|
+
Object.keys(_layers).forEach(name => {
|
|
668
|
+
_layers[name].remove();
|
|
669
|
+
delete _layers[name];
|
|
670
|
+
});
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
resetLayers: function() {
|
|
674
|
+
const { _layers } = this;
|
|
675
|
+
Object.keys(_layers).forEach(name => {
|
|
676
|
+
_layers[name].removePivots();
|
|
677
|
+
});
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
update: function() {
|
|
681
|
+
|
|
682
|
+
if (this._background) {
|
|
683
|
+
this.updateBackgroundImage(this._background);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return this;
|
|
687
|
+
},
|
|
688
|
+
|
|
689
|
+
scale: function(sx, sy, data) {
|
|
690
|
+
const ctm = this.matrix();
|
|
691
|
+
// getter
|
|
692
|
+
if (sx === undefined) {
|
|
693
|
+
return V.matrixToScale(ctm);
|
|
694
|
+
}
|
|
695
|
+
// setter
|
|
696
|
+
if (sy === undefined) {
|
|
697
|
+
sy = sx;
|
|
698
|
+
}
|
|
699
|
+
sx = Math.max(sx || 0, this.MIN_SCALE);
|
|
700
|
+
sy = Math.max(sy || 0, this.MIN_SCALE);
|
|
701
|
+
ctm.a = sx;
|
|
702
|
+
ctm.d = sy;
|
|
703
|
+
this.matrix(ctm, data);
|
|
704
|
+
return this;
|
|
705
|
+
},
|
|
706
|
+
|
|
707
|
+
scaleUniformAtPoint: function(scale, point, data) {
|
|
708
|
+
const { a: sx, d: sy, e: tx, f: ty } = this.matrix();
|
|
709
|
+
scale = Math.max(scale || 0, this.MIN_SCALE);
|
|
710
|
+
if (scale === sx && scale === sy) {
|
|
711
|
+
// The scale is the same as the current one.
|
|
712
|
+
return this;
|
|
713
|
+
}
|
|
714
|
+
const matrix = V.createSVGMatrix()
|
|
715
|
+
.translate(
|
|
716
|
+
tx - point.x * (scale - sx),
|
|
717
|
+
ty - point.y * (scale - sy)
|
|
718
|
+
)
|
|
719
|
+
.scale(scale, scale);
|
|
720
|
+
this.matrix(matrix, data);
|
|
721
|
+
return this;
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
translate: function(tx, ty, data) {
|
|
725
|
+
const ctm = this.matrix();
|
|
726
|
+
// getter
|
|
727
|
+
if (tx === undefined) {
|
|
728
|
+
return V.matrixToTranslate(ctm);
|
|
729
|
+
}
|
|
730
|
+
// setter
|
|
731
|
+
tx || (tx = 0);
|
|
732
|
+
ty || (ty = 0);
|
|
733
|
+
if (ctm.e === tx && ctm.f === ty) return this;
|
|
734
|
+
ctm.e = tx;
|
|
735
|
+
ctm.f = ty;
|
|
736
|
+
this.matrix(ctm, data);
|
|
737
|
+
return this;
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
matrix: function(ctm, data = {}) {
|
|
741
|
+
|
|
742
|
+
var viewport = this.layers;
|
|
743
|
+
|
|
744
|
+
// Getter:
|
|
745
|
+
if (ctm === undefined) {
|
|
746
|
+
|
|
747
|
+
var transformString = viewport.getAttribute('transform');
|
|
748
|
+
|
|
749
|
+
if ((this._viewportTransformString || null) === transformString) {
|
|
750
|
+
// It's ok to return the cached matrix. The transform attribute has not changed since
|
|
751
|
+
// the matrix was stored.
|
|
752
|
+
ctm = this._viewportMatrix;
|
|
753
|
+
} else {
|
|
754
|
+
// The viewport transform attribute has changed. Measure the matrix and cache again.
|
|
755
|
+
ctm = viewport.getCTM();
|
|
756
|
+
this._viewportMatrix = ctm;
|
|
757
|
+
this._viewportTransformString = transformString;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Clone the cached current transformation matrix.
|
|
761
|
+
// If no matrix previously stored the identity matrix is returned.
|
|
762
|
+
return V.createSVGMatrix(ctm);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Setter:
|
|
766
|
+
const prev = this.matrix();
|
|
767
|
+
const current = V.createSVGMatrix(ctm);
|
|
768
|
+
const currentTransformString = this._viewportTransformString;
|
|
769
|
+
const ctmString = V.matrixToTransformString(current);
|
|
770
|
+
if (ctmString === currentTransformString) {
|
|
771
|
+
// The new transform string is the same as the current one.
|
|
772
|
+
// No need to update the transform attribute.
|
|
773
|
+
return this;
|
|
774
|
+
}
|
|
775
|
+
if (!currentTransformString && V.matrixToTransformString() === ctmString) {
|
|
776
|
+
// The current transform string is empty and the new one is an identity matrix.
|
|
777
|
+
// No need to update the transform attribute.
|
|
778
|
+
return this;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const { a, d, e, f } = current;
|
|
782
|
+
|
|
783
|
+
viewport.setAttribute('transform', ctmString);
|
|
784
|
+
this._viewportMatrix = current;
|
|
785
|
+
this._viewportTransformString = viewport.getAttribute('transform');
|
|
786
|
+
|
|
787
|
+
// scale event
|
|
788
|
+
if (a !== prev.a || d !== prev.d) {
|
|
789
|
+
this.trigger('scale', a, d, data);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// translate event
|
|
793
|
+
if (e !== prev.e || f !== prev.f) {
|
|
794
|
+
this.trigger('translate', e, f, data);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
this.trigger('transform', current, data);
|
|
798
|
+
return this;
|
|
799
|
+
},
|
|
800
|
+
|
|
801
|
+
clientMatrix: function() {
|
|
802
|
+
|
|
803
|
+
return V.createSVGMatrix(this.cells.getScreenCTM());
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
requestConnectedLinksUpdate: function(view, priority, opt) {
|
|
807
|
+
if (view instanceof CellView) {
|
|
808
|
+
var model = view.model;
|
|
809
|
+
var links = this.model.getConnectedLinks(model);
|
|
810
|
+
for (var j = 0, n = links.length; j < n; j++) {
|
|
811
|
+
var link = links[j];
|
|
812
|
+
var linkView = this.findViewByModel(link);
|
|
813
|
+
if (!linkView) continue;
|
|
814
|
+
var flagLabels = ['UPDATE'];
|
|
815
|
+
if (link.getTargetCell() === model) flagLabels.push('TARGET');
|
|
816
|
+
if (link.getSourceCell() === model) flagLabels.push('SOURCE');
|
|
817
|
+
var nextPriority = Math.max(priority + 1, linkView.UPDATE_PRIORITY);
|
|
818
|
+
this.scheduleViewUpdate(linkView, linkView.getFlag(flagLabels), nextPriority, opt);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
|
|
823
|
+
forcePostponedViewUpdate: function(view, flag) {
|
|
824
|
+
if (!view || !(view instanceof CellView)) return false;
|
|
825
|
+
var model = view.model;
|
|
826
|
+
if (model.isElement()) return false;
|
|
827
|
+
if ((flag & view.getFlag(['SOURCE', 'TARGET'])) === 0) {
|
|
828
|
+
var dumpOptions = { silent: true };
|
|
829
|
+
// LinkView is waiting for the target or the source cellView to be rendered
|
|
830
|
+
// This can happen when the cells are not in the viewport.
|
|
831
|
+
var sourceFlag = 0;
|
|
832
|
+
var sourceView = this.findViewByModel(model.getSourceCell());
|
|
833
|
+
if (sourceView && !this.isViewMounted(sourceView)) {
|
|
834
|
+
sourceFlag = this.dumpView(sourceView, dumpOptions);
|
|
835
|
+
view.updateEndMagnet('source');
|
|
836
|
+
}
|
|
837
|
+
var targetFlag = 0;
|
|
838
|
+
var targetView = this.findViewByModel(model.getTargetCell());
|
|
839
|
+
if (targetView && !this.isViewMounted(targetView)) {
|
|
840
|
+
targetFlag = this.dumpView(targetView, dumpOptions);
|
|
841
|
+
view.updateEndMagnet('target');
|
|
842
|
+
}
|
|
843
|
+
if (sourceFlag === 0 && targetFlag === 0) {
|
|
844
|
+
// If leftover flag is 0, all view updates were done.
|
|
845
|
+
return !this.dumpView(view, dumpOptions);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return false;
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
requestViewUpdate: function(view, flag, priority, opt) {
|
|
852
|
+
opt || (opt = {});
|
|
853
|
+
this.scheduleViewUpdate(view, flag, priority, opt);
|
|
854
|
+
var isAsync = this.isAsync();
|
|
855
|
+
if (this.isFrozen() || (isAsync && opt.async !== false)) return;
|
|
856
|
+
if (this.model.hasActiveBatch(this.UPDATE_DELAYING_BATCHES)) return;
|
|
857
|
+
var stats = this.updateViews(opt);
|
|
858
|
+
if (isAsync) this.notifyAfterRender(stats, opt);
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
scheduleViewUpdate: function(view, type, priority, opt) {
|
|
862
|
+
const { _updates: updates, options } = this;
|
|
863
|
+
if (updates.idle) {
|
|
864
|
+
if (options.autoFreeze) {
|
|
865
|
+
updates.idle = false;
|
|
866
|
+
this.unfreeze();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const { FLAG_REMOVE, FLAG_INSERT, UPDATE_PRIORITY, cid } = view;
|
|
870
|
+
let priorityUpdates = updates.priorities[priority];
|
|
871
|
+
if (!priorityUpdates) priorityUpdates = updates.priorities[priority] = {};
|
|
872
|
+
// Move higher priority updates to this priority
|
|
873
|
+
if (priority > UPDATE_PRIORITY) {
|
|
874
|
+
// Not the default priority for this view. It's most likely a link view
|
|
875
|
+
// connected to another link view, which triggered the update.
|
|
876
|
+
// TODO: If there is an update scheduled with a lower priority already, we should
|
|
877
|
+
// change the requested priority to the lowest one. Does not seem to be critical
|
|
878
|
+
// right now, as it "only" results in multiple updates on the same view.
|
|
879
|
+
for (let i = priority - 1; i >= UPDATE_PRIORITY; i--) {
|
|
880
|
+
const prevPriorityUpdates = updates.priorities[i];
|
|
881
|
+
if (!prevPriorityUpdates || !(cid in prevPriorityUpdates)) continue;
|
|
882
|
+
priorityUpdates[cid] |= prevPriorityUpdates[cid];
|
|
883
|
+
delete prevPriorityUpdates[cid];
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
let currentType = priorityUpdates[cid] || 0;
|
|
887
|
+
// Prevent cycling
|
|
888
|
+
if ((currentType & type) === type) return;
|
|
889
|
+
if (!currentType) updates.count++;
|
|
890
|
+
if (type & FLAG_REMOVE && currentType & FLAG_INSERT) {
|
|
891
|
+
// When a view is removed we need to remove the insert flag as this is a reinsert
|
|
892
|
+
priorityUpdates[cid] ^= FLAG_INSERT;
|
|
893
|
+
} else if (type & FLAG_INSERT && currentType & FLAG_REMOVE) {
|
|
894
|
+
// When a view is added we need to remove the remove flag as this is view was previously removed
|
|
895
|
+
priorityUpdates[cid] ^= FLAG_REMOVE;
|
|
896
|
+
}
|
|
897
|
+
priorityUpdates[cid] |= type;
|
|
898
|
+
const viewUpdateFn = options.onViewUpdate;
|
|
899
|
+
if (typeof viewUpdateFn === 'function') viewUpdateFn.call(this, view, type, priority, opt || {}, this);
|
|
900
|
+
},
|
|
901
|
+
|
|
902
|
+
dumpViewUpdate: function(view) {
|
|
903
|
+
if (!view) return 0;
|
|
904
|
+
var updates = this._updates;
|
|
905
|
+
var cid = view.cid;
|
|
906
|
+
var priorityUpdates = updates.priorities[view.UPDATE_PRIORITY];
|
|
907
|
+
var flag = this.registerMountedView(view) | priorityUpdates[cid];
|
|
908
|
+
delete priorityUpdates[cid];
|
|
909
|
+
return flag;
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
dumpView: function(view, opt = {}) {
|
|
913
|
+
const flag = this.dumpViewUpdate(view);
|
|
914
|
+
if (!flag) return 0;
|
|
915
|
+
const shouldNotify = !opt.silent;
|
|
916
|
+
if (shouldNotify) this.notifyBeforeRender(opt);
|
|
917
|
+
const leftover = this.updateView(view, flag, opt);
|
|
918
|
+
if (shouldNotify) {
|
|
919
|
+
const stats = { updated: 1, priority: view.UPDATE_PRIORITY };
|
|
920
|
+
this.notifyAfterRender(stats, opt);
|
|
921
|
+
}
|
|
922
|
+
return leftover;
|
|
923
|
+
},
|
|
924
|
+
|
|
925
|
+
updateView: function(view, flag, opt) {
|
|
926
|
+
if (!view) return 0;
|
|
927
|
+
const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT, model } = view;
|
|
928
|
+
if (view instanceof CellView) {
|
|
929
|
+
if (flag & FLAG_REMOVE) {
|
|
930
|
+
this.removeView(model);
|
|
931
|
+
return 0;
|
|
932
|
+
}
|
|
933
|
+
if (flag & FLAG_INSERT) {
|
|
934
|
+
const isInitialInsert = !!(flag & FLAG_INIT);
|
|
935
|
+
if (isInitialInsert) {
|
|
936
|
+
flag ^= FLAG_INIT;
|
|
937
|
+
}
|
|
938
|
+
this.insertView(view, isInitialInsert);
|
|
939
|
+
flag ^= FLAG_INSERT;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (!flag) return 0;
|
|
943
|
+
return view.confirmUpdate(flag, opt || {});
|
|
944
|
+
},
|
|
945
|
+
|
|
946
|
+
requireView: function(model, opt) {
|
|
947
|
+
var view = this.findViewByModel(model);
|
|
948
|
+
if (!view) return null;
|
|
949
|
+
this.dumpView(view, opt);
|
|
950
|
+
return view;
|
|
951
|
+
},
|
|
952
|
+
|
|
953
|
+
registerUnmountedView: function(view) {
|
|
954
|
+
var cid = view.cid;
|
|
955
|
+
var updates = this._updates;
|
|
956
|
+
if (cid in updates.unmounted) return 0;
|
|
957
|
+
var flag = updates.unmounted[cid] |= view.FLAG_INSERT;
|
|
958
|
+
updates.unmountedCids.push(cid);
|
|
959
|
+
delete updates.mounted[cid];
|
|
960
|
+
return flag;
|
|
961
|
+
},
|
|
962
|
+
|
|
963
|
+
registerMountedView: function(view) {
|
|
964
|
+
var cid = view.cid;
|
|
965
|
+
var updates = this._updates;
|
|
966
|
+
if (cid in updates.mounted) return 0;
|
|
967
|
+
updates.mounted[cid] = true;
|
|
968
|
+
updates.mountedCids.push(cid);
|
|
969
|
+
var flag = updates.unmounted[cid] || 0;
|
|
970
|
+
delete updates.unmounted[cid];
|
|
971
|
+
return flag;
|
|
972
|
+
},
|
|
973
|
+
|
|
974
|
+
isViewMounted: function(view) {
|
|
975
|
+
if (!view) return false;
|
|
976
|
+
var cid = view.cid;
|
|
977
|
+
var updates = this._updates;
|
|
978
|
+
return (cid in updates.mounted);
|
|
979
|
+
},
|
|
980
|
+
|
|
981
|
+
dumpViews: function(opt) {
|
|
982
|
+
var passingOpt = defaults({}, opt, { viewport: null });
|
|
983
|
+
this.checkViewport(passingOpt);
|
|
984
|
+
this.updateViews(passingOpt);
|
|
985
|
+
},
|
|
986
|
+
|
|
987
|
+
// Synchronous views update
|
|
988
|
+
updateViews: function(opt) {
|
|
989
|
+
this.notifyBeforeRender(opt);
|
|
990
|
+
let batchStats;
|
|
991
|
+
let updateCount = 0;
|
|
992
|
+
let batchCount = 0;
|
|
993
|
+
let priority = MIN_PRIORITY;
|
|
994
|
+
do {
|
|
995
|
+
batchCount++;
|
|
996
|
+
batchStats = this.updateViewsBatch(opt);
|
|
997
|
+
updateCount += batchStats.updated;
|
|
998
|
+
priority = Math.min(batchStats.priority, priority);
|
|
999
|
+
} while (!batchStats.empty);
|
|
1000
|
+
const stats = { updated: updateCount, batches: batchCount, priority };
|
|
1001
|
+
this.notifyAfterRender(stats, opt);
|
|
1002
|
+
return stats;
|
|
1003
|
+
},
|
|
1004
|
+
|
|
1005
|
+
hasScheduledUpdates: function() {
|
|
1006
|
+
const priorities = this._updates.priorities;
|
|
1007
|
+
const priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
|
|
1008
|
+
let i = priorityIndexes.length;
|
|
1009
|
+
while (i > 0 && i--) {
|
|
1010
|
+
// a faster way how to check if an object is empty
|
|
1011
|
+
for (let _key in priorities[priorityIndexes[i]]) return true;
|
|
1012
|
+
}
|
|
1013
|
+
return false;
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
updateViewsAsync: function(opt, data) {
|
|
1017
|
+
opt || (opt = {});
|
|
1018
|
+
data || (data = { processed: 0, priority: MIN_PRIORITY });
|
|
1019
|
+
const { _updates: updates, options } = this;
|
|
1020
|
+
const id = updates.id;
|
|
1021
|
+
if (id) {
|
|
1022
|
+
cancelFrame(id);
|
|
1023
|
+
if (data.processed === 0 && this.hasScheduledUpdates()) {
|
|
1024
|
+
this.notifyBeforeRender(opt);
|
|
1025
|
+
}
|
|
1026
|
+
const stats = this.updateViewsBatch(opt);
|
|
1027
|
+
const passingOpt = defaults({}, opt, {
|
|
1028
|
+
mountBatchSize: MOUNT_BATCH_SIZE - stats.mounted,
|
|
1029
|
+
unmountBatchSize: MOUNT_BATCH_SIZE - stats.unmounted
|
|
1030
|
+
});
|
|
1031
|
+
const checkStats = this.checkViewport(passingOpt);
|
|
1032
|
+
const unmountCount = checkStats.unmounted;
|
|
1033
|
+
const mountCount = checkStats.mounted;
|
|
1034
|
+
let processed = data.processed;
|
|
1035
|
+
const total = updates.count;
|
|
1036
|
+
if (stats.updated > 0) {
|
|
1037
|
+
// Some updates have been just processed
|
|
1038
|
+
processed += stats.updated + stats.unmounted;
|
|
1039
|
+
stats.processed = processed;
|
|
1040
|
+
data.priority = Math.min(stats.priority, data.priority);
|
|
1041
|
+
if (stats.empty && mountCount === 0) {
|
|
1042
|
+
stats.unmounted += unmountCount;
|
|
1043
|
+
stats.mounted += mountCount;
|
|
1044
|
+
stats.priority = data.priority;
|
|
1045
|
+
this.notifyAfterRender(stats, opt);
|
|
1046
|
+
data.processed = 0;
|
|
1047
|
+
data.priority = MIN_PRIORITY;
|
|
1048
|
+
updates.count = 0;
|
|
1049
|
+
} else {
|
|
1050
|
+
data.processed = processed;
|
|
1051
|
+
}
|
|
1052
|
+
} else {
|
|
1053
|
+
if (!updates.idle) {
|
|
1054
|
+
if (options.autoFreeze) {
|
|
1055
|
+
this.freeze();
|
|
1056
|
+
updates.idle = true;
|
|
1057
|
+
this.trigger('render:idle', opt);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// Progress callback
|
|
1062
|
+
const progressFn = opt.progress;
|
|
1063
|
+
if (total && typeof progressFn === 'function') {
|
|
1064
|
+
progressFn.call(this, stats.empty, processed, total, stats, this);
|
|
1065
|
+
}
|
|
1066
|
+
// The current frame could have been canceled in a callback
|
|
1067
|
+
if (updates.id !== id) return;
|
|
1068
|
+
}
|
|
1069
|
+
if (updates.disabled) {
|
|
1070
|
+
throw new Error('dia.Paper: can not unfreeze the paper after it was removed');
|
|
1071
|
+
}
|
|
1072
|
+
updates.id = nextFrame(this.updateViewsAsync, this, opt, data);
|
|
1073
|
+
},
|
|
1074
|
+
|
|
1075
|
+
notifyBeforeRender: function(opt = {}) {
|
|
1076
|
+
let beforeFn = opt.beforeRender;
|
|
1077
|
+
if (typeof beforeFn !== 'function') {
|
|
1078
|
+
beforeFn = this.options.beforeRender;
|
|
1079
|
+
if (typeof beforeFn !== 'function') return;
|
|
1080
|
+
}
|
|
1081
|
+
beforeFn.call(this, opt, this);
|
|
1082
|
+
},
|
|
1083
|
+
|
|
1084
|
+
notifyAfterRender: function(stats, opt = {}) {
|
|
1085
|
+
let afterFn = opt.afterRender;
|
|
1086
|
+
if (typeof afterFn !== 'function') {
|
|
1087
|
+
afterFn = this.options.afterRender;
|
|
1088
|
+
}
|
|
1089
|
+
if (typeof afterFn === 'function') {
|
|
1090
|
+
afterFn.call(this, stats, opt, this);
|
|
1091
|
+
}
|
|
1092
|
+
this.trigger('render:done', stats, opt);
|
|
1093
|
+
},
|
|
1094
|
+
|
|
1095
|
+
updateViewsBatch: function(opt) {
|
|
1096
|
+
opt || (opt = {});
|
|
1097
|
+
var batchSize = opt.batchSize || UPDATE_BATCH_SIZE;
|
|
1098
|
+
var updates = this._updates;
|
|
1099
|
+
var updateCount = 0;
|
|
1100
|
+
var postponeCount = 0;
|
|
1101
|
+
var unmountCount = 0;
|
|
1102
|
+
var mountCount = 0;
|
|
1103
|
+
var maxPriority = MIN_PRIORITY;
|
|
1104
|
+
var empty = true;
|
|
1105
|
+
var options = this.options;
|
|
1106
|
+
var priorities = updates.priorities;
|
|
1107
|
+
var viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
|
|
1108
|
+
if (typeof viewportFn !== 'function') viewportFn = null;
|
|
1109
|
+
var postponeViewFn = options.onViewPostponed;
|
|
1110
|
+
if (typeof postponeViewFn !== 'function') postponeViewFn = null;
|
|
1111
|
+
var priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
|
|
1112
|
+
main: for (var i = 0, n = priorityIndexes.length; i < n; i++) {
|
|
1113
|
+
var priority = +priorityIndexes[i];
|
|
1114
|
+
var priorityUpdates = priorities[priority];
|
|
1115
|
+
for (var cid in priorityUpdates) {
|
|
1116
|
+
if (updateCount >= batchSize) {
|
|
1117
|
+
empty = false;
|
|
1118
|
+
break main;
|
|
1119
|
+
}
|
|
1120
|
+
var view = views[cid];
|
|
1121
|
+
if (!view) {
|
|
1122
|
+
// This should not occur
|
|
1123
|
+
delete priorityUpdates[cid];
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
var currentFlag = priorityUpdates[cid];
|
|
1127
|
+
if ((currentFlag & view.FLAG_REMOVE) === 0) {
|
|
1128
|
+
// We should never check a view for viewport if we are about to remove the view
|
|
1129
|
+
var isDetached = cid in updates.unmounted;
|
|
1130
|
+
if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, !isDetached, this)) {
|
|
1131
|
+
// Unmount View
|
|
1132
|
+
if (!isDetached) {
|
|
1133
|
+
this.registerUnmountedView(view);
|
|
1134
|
+
this.detachView(view);
|
|
1135
|
+
}
|
|
1136
|
+
updates.unmounted[cid] |= currentFlag;
|
|
1137
|
+
delete priorityUpdates[cid];
|
|
1138
|
+
unmountCount++;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
// Mount View
|
|
1142
|
+
if (isDetached) {
|
|
1143
|
+
currentFlag |= view.FLAG_INSERT;
|
|
1144
|
+
mountCount++;
|
|
1145
|
+
}
|
|
1146
|
+
currentFlag |= this.registerMountedView(view);
|
|
1147
|
+
}
|
|
1148
|
+
var leftoverFlag = this.updateView(view, currentFlag, opt);
|
|
1149
|
+
if (leftoverFlag > 0) {
|
|
1150
|
+
// View update has not finished completely
|
|
1151
|
+
priorityUpdates[cid] = leftoverFlag;
|
|
1152
|
+
if (!postponeViewFn || !postponeViewFn.call(this, view, leftoverFlag, this) || priorityUpdates[cid]) {
|
|
1153
|
+
postponeCount++;
|
|
1154
|
+
empty = false;
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (maxPriority > priority) maxPriority = priority;
|
|
1159
|
+
updateCount++;
|
|
1160
|
+
delete priorityUpdates[cid];
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
priority: maxPriority,
|
|
1165
|
+
updated: updateCount,
|
|
1166
|
+
postponed: postponeCount,
|
|
1167
|
+
unmounted: unmountCount,
|
|
1168
|
+
mounted: mountCount,
|
|
1169
|
+
empty: empty
|
|
1170
|
+
};
|
|
1171
|
+
},
|
|
1172
|
+
|
|
1173
|
+
getUnmountedViews: function() {
|
|
1174
|
+
const updates = this._updates;
|
|
1175
|
+
const unmountedCids = Object.keys(updates.unmounted);
|
|
1176
|
+
const n = unmountedCids.length;
|
|
1177
|
+
const unmountedViews = new Array(n);
|
|
1178
|
+
for (var i = 0; i < n; i++) {
|
|
1179
|
+
unmountedViews[i] = views[unmountedCids[i]];
|
|
1180
|
+
}
|
|
1181
|
+
return unmountedViews;
|
|
1182
|
+
},
|
|
1183
|
+
|
|
1184
|
+
getMountedViews: function() {
|
|
1185
|
+
const updates = this._updates;
|
|
1186
|
+
const mountedCids = Object.keys(updates.mounted);
|
|
1187
|
+
const n = mountedCids.length;
|
|
1188
|
+
const mountedViews = new Array(n);
|
|
1189
|
+
for (var i = 0; i < n; i++) {
|
|
1190
|
+
mountedViews[i] = views[mountedCids[i]];
|
|
1191
|
+
}
|
|
1192
|
+
return mountedViews;
|
|
1193
|
+
},
|
|
1194
|
+
|
|
1195
|
+
checkUnmountedViews: function(viewportFn, opt) {
|
|
1196
|
+
opt || (opt = {});
|
|
1197
|
+
var mountCount = 0;
|
|
1198
|
+
if (typeof viewportFn !== 'function') viewportFn = null;
|
|
1199
|
+
var batchSize = 'mountBatchSize' in opt ? opt.mountBatchSize : Infinity;
|
|
1200
|
+
var updates = this._updates;
|
|
1201
|
+
var unmountedCids = updates.unmountedCids;
|
|
1202
|
+
var unmounted = updates.unmounted;
|
|
1203
|
+
for (var i = 0, n = Math.min(unmountedCids.length, batchSize); i < n; i++) {
|
|
1204
|
+
var cid = unmountedCids[i];
|
|
1205
|
+
if (!(cid in unmounted)) continue;
|
|
1206
|
+
var view = views[cid];
|
|
1207
|
+
if (!view) continue;
|
|
1208
|
+
if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, false, this)) {
|
|
1209
|
+
// Push at the end of all unmounted ids, so this can be check later again
|
|
1210
|
+
unmountedCids.push(cid);
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
mountCount++;
|
|
1214
|
+
var flag = this.registerMountedView(view);
|
|
1215
|
+
if (flag) this.scheduleViewUpdate(view, flag, view.UPDATE_PRIORITY, { mounting: true });
|
|
1216
|
+
}
|
|
1217
|
+
// Get rid of views, that have been mounted
|
|
1218
|
+
unmountedCids.splice(0, i);
|
|
1219
|
+
return mountCount;
|
|
1220
|
+
},
|
|
1221
|
+
|
|
1222
|
+
checkMountedViews: function(viewportFn, opt) {
|
|
1223
|
+
opt || (opt = {});
|
|
1224
|
+
var unmountCount = 0;
|
|
1225
|
+
if (typeof viewportFn !== 'function') return unmountCount;
|
|
1226
|
+
var batchSize = 'unmountBatchSize' in opt ? opt.unmountBatchSize : Infinity;
|
|
1227
|
+
var updates = this._updates;
|
|
1228
|
+
var mountedCids = updates.mountedCids;
|
|
1229
|
+
var mounted = updates.mounted;
|
|
1230
|
+
for (var i = 0, n = Math.min(mountedCids.length, batchSize); i < n; i++) {
|
|
1231
|
+
var cid = mountedCids[i];
|
|
1232
|
+
if (!(cid in mounted)) continue;
|
|
1233
|
+
var view = views[cid];
|
|
1234
|
+
if (!view) continue;
|
|
1235
|
+
if (!view.DETACHABLE || viewportFn.call(this, view, true, this)) {
|
|
1236
|
+
// Push at the end of all mounted ids, so this can be check later again
|
|
1237
|
+
mountedCids.push(cid);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
unmountCount++;
|
|
1241
|
+
var flag = this.registerUnmountedView(view);
|
|
1242
|
+
if (flag) this.detachView(view);
|
|
1243
|
+
}
|
|
1244
|
+
// Get rid of views, that have been unmounted
|
|
1245
|
+
mountedCids.splice(0, i);
|
|
1246
|
+
return unmountCount;
|
|
1247
|
+
},
|
|
1248
|
+
|
|
1249
|
+
checkViewVisibility: function(cellView, opt = {}) {
|
|
1250
|
+
let viewportFn = 'viewport' in opt ? opt.viewport : this.options.viewport;
|
|
1251
|
+
if (typeof viewportFn !== 'function') viewportFn = null;
|
|
1252
|
+
const updates = this._updates;
|
|
1253
|
+
const { mounted, unmounted } = updates;
|
|
1254
|
+
const visible = !cellView.DETACHABLE || !viewportFn || viewportFn.call(this, cellView, false, this);
|
|
1255
|
+
|
|
1256
|
+
let isUnmounted = false;
|
|
1257
|
+
let isMounted = false;
|
|
1258
|
+
|
|
1259
|
+
if (cellView.cid in mounted && !visible) {
|
|
1260
|
+
const flag = this.registerUnmountedView(cellView);
|
|
1261
|
+
if (flag) this.detachView(cellView);
|
|
1262
|
+
const i = updates.mountedCids.indexOf(cellView.cid);
|
|
1263
|
+
updates.mountedCids.splice(i, 1);
|
|
1264
|
+
isUnmounted = true;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (!isUnmounted && cellView.cid in unmounted && visible) {
|
|
1268
|
+
const i = updates.unmountedCids.indexOf(cellView.cid);
|
|
1269
|
+
updates.unmountedCids.splice(i, 1);
|
|
1270
|
+
var flag = this.registerMountedView(cellView);
|
|
1271
|
+
if (flag) this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, { mounting: true });
|
|
1272
|
+
isMounted = true;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return {
|
|
1276
|
+
mounted: isMounted ? 1 : 0,
|
|
1277
|
+
unmounted: isUnmounted ? 1 : 0
|
|
1278
|
+
};
|
|
1279
|
+
},
|
|
1280
|
+
|
|
1281
|
+
checkViewport: function(opt) {
|
|
1282
|
+
var passingOpt = defaults({}, opt, {
|
|
1283
|
+
mountBatchSize: Infinity,
|
|
1284
|
+
unmountBatchSize: Infinity
|
|
1285
|
+
});
|
|
1286
|
+
var viewportFn = 'viewport' in passingOpt ? passingOpt.viewport : this.options.viewport;
|
|
1287
|
+
var unmountedCount = this.checkMountedViews(viewportFn, passingOpt);
|
|
1288
|
+
if (unmountedCount > 0) {
|
|
1289
|
+
// Do not check views, that have been just unmounted and pushed at the end of the cids array
|
|
1290
|
+
var unmountedCids = this._updates.unmountedCids;
|
|
1291
|
+
passingOpt.mountBatchSize = Math.min(unmountedCids.length - unmountedCount, passingOpt.mountBatchSize);
|
|
1292
|
+
}
|
|
1293
|
+
var mountedCount = this.checkUnmountedViews(viewportFn, passingOpt);
|
|
1294
|
+
return {
|
|
1295
|
+
mounted: mountedCount,
|
|
1296
|
+
unmounted: unmountedCount
|
|
1297
|
+
};
|
|
1298
|
+
},
|
|
1299
|
+
|
|
1300
|
+
freeze: function(opt) {
|
|
1301
|
+
opt || (opt = {});
|
|
1302
|
+
var updates = this._updates;
|
|
1303
|
+
var key = opt.key;
|
|
1304
|
+
var isFrozen = this.options.frozen;
|
|
1305
|
+
var freezeKey = updates.freezeKey;
|
|
1306
|
+
if (key && key !== freezeKey) {
|
|
1307
|
+
// key passed, but the paper is already freezed with another key
|
|
1308
|
+
if (isFrozen && freezeKey) return;
|
|
1309
|
+
updates.freezeKey = key;
|
|
1310
|
+
updates.keyFrozen = isFrozen;
|
|
1311
|
+
}
|
|
1312
|
+
this.options.frozen = true;
|
|
1313
|
+
var id = updates.id;
|
|
1314
|
+
updates.id = null;
|
|
1315
|
+
if (this.isAsync() && id) cancelFrame(id);
|
|
1316
|
+
},
|
|
1317
|
+
|
|
1318
|
+
unfreeze: function(opt) {
|
|
1319
|
+
opt || (opt = {});
|
|
1320
|
+
var updates = this._updates;
|
|
1321
|
+
var key = opt.key;
|
|
1322
|
+
var freezeKey = updates.freezeKey;
|
|
1323
|
+
// key passed, but the paper is already freezed with another key
|
|
1324
|
+
if (key && freezeKey && key !== freezeKey) return;
|
|
1325
|
+
updates.freezeKey = null;
|
|
1326
|
+
// key passed, but the paper is already freezed
|
|
1327
|
+
if (key && key === freezeKey && updates.keyFrozen) return;
|
|
1328
|
+
if (this.isAsync()) {
|
|
1329
|
+
this.freeze();
|
|
1330
|
+
this.updateViewsAsync(opt);
|
|
1331
|
+
} else {
|
|
1332
|
+
this.updateViews(opt);
|
|
1333
|
+
}
|
|
1334
|
+
this.options.frozen = updates.keyFrozen = false;
|
|
1335
|
+
if (updates.sort) {
|
|
1336
|
+
this.sortViews();
|
|
1337
|
+
updates.sort = false;
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
|
|
1341
|
+
isAsync: function() {
|
|
1342
|
+
return !!this.options.async;
|
|
1343
|
+
},
|
|
1344
|
+
|
|
1345
|
+
isFrozen: function() {
|
|
1346
|
+
return !!this.options.frozen;
|
|
1347
|
+
},
|
|
1348
|
+
|
|
1349
|
+
isExactSorting: function() {
|
|
1350
|
+
return this.options.sorting === sortingTypes.EXACT;
|
|
1351
|
+
},
|
|
1352
|
+
|
|
1353
|
+
onRemove: function() {
|
|
1354
|
+
|
|
1355
|
+
this.freeze();
|
|
1356
|
+
this._updates.disabled = true;
|
|
1357
|
+
//clean up all DOM elements/views to prevent memory leaks
|
|
1358
|
+
this.removeLayers();
|
|
1359
|
+
this.removeViews();
|
|
1360
|
+
},
|
|
1361
|
+
|
|
1362
|
+
getComputedSize: function() {
|
|
1363
|
+
|
|
1364
|
+
var options = this.options;
|
|
1365
|
+
var w = options.width;
|
|
1366
|
+
var h = options.height;
|
|
1367
|
+
if (!isNumber(w)) w = this.el.clientWidth;
|
|
1368
|
+
if (!isNumber(h)) h = this.el.clientHeight;
|
|
1369
|
+
return { width: w, height: h };
|
|
1370
|
+
},
|
|
1371
|
+
|
|
1372
|
+
setDimensions: function(width, height, data = {}) {
|
|
1373
|
+
const { options } = this;
|
|
1374
|
+
const { width: currentWidth, height: currentHeight } = options;
|
|
1375
|
+
let w = (width === undefined) ? currentWidth : width;
|
|
1376
|
+
let h = (height === undefined) ? currentHeight : height;
|
|
1377
|
+
if (currentWidth === w && currentHeight === h) return;
|
|
1378
|
+
options.width = w;
|
|
1379
|
+
options.height = h;
|
|
1380
|
+
this._setDimensions();
|
|
1381
|
+
const computedSize = this.getComputedSize();
|
|
1382
|
+
this.trigger('resize', computedSize.width, computedSize.height, data);
|
|
1383
|
+
},
|
|
1384
|
+
|
|
1385
|
+
_setDimensions: function() {
|
|
1386
|
+
const { options } = this;
|
|
1387
|
+
let w = options.width;
|
|
1388
|
+
let h = options.height;
|
|
1389
|
+
if (isNumber(w)) w = `${Math.round(w)}px`;
|
|
1390
|
+
if (isNumber(h)) h = `${Math.round(h)}px`;
|
|
1391
|
+
this.$el.css({
|
|
1392
|
+
width: (w === null) ? '' : w,
|
|
1393
|
+
height: (h === null) ? '' : h
|
|
1394
|
+
});
|
|
1395
|
+
},
|
|
1396
|
+
|
|
1397
|
+
// Expand/shrink the paper to fit the content.
|
|
1398
|
+
// Alternatively signature function(opt)
|
|
1399
|
+
fitToContent: function(gridWidth, gridHeight, padding, opt) {
|
|
1400
|
+
|
|
1401
|
+
if (isObject(gridWidth)) {
|
|
1402
|
+
// first parameter is an option object
|
|
1403
|
+
opt = gridWidth;
|
|
1404
|
+
} else {
|
|
1405
|
+
// Support for a deprecated signature
|
|
1406
|
+
opt = assign({ gridWidth, gridHeight, padding }, opt);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const { x, y, width, height } = this.getFitToContentArea(opt);
|
|
1410
|
+
const { sx, sy } = this.scale();
|
|
1411
|
+
|
|
1412
|
+
this.translate(-x * sx, -y * sy, opt);
|
|
1413
|
+
this.setDimensions(width * sx, height * sy, opt);
|
|
1414
|
+
|
|
1415
|
+
return new Rect(x, y, width, height);
|
|
1416
|
+
},
|
|
1417
|
+
|
|
1418
|
+
getFitToContentArea: function(opt = {}) {
|
|
1419
|
+
|
|
1420
|
+
// Calculate the paper size to accommodate all the graph's elements.
|
|
1421
|
+
|
|
1422
|
+
const gridWidth = opt.gridWidth || 1;
|
|
1423
|
+
const gridHeight = opt.gridHeight || 1;
|
|
1424
|
+
const padding = normalizeSides(opt.padding || 0);
|
|
1425
|
+
|
|
1426
|
+
const minWidth = Math.max(opt.minWidth || 0, gridWidth);
|
|
1427
|
+
const minHeight = Math.max(opt.minHeight || 0, gridHeight);
|
|
1428
|
+
const maxWidth = opt.maxWidth || Number.MAX_VALUE;
|
|
1429
|
+
const maxHeight = opt.maxHeight || Number.MAX_VALUE;
|
|
1430
|
+
const newOrigin = opt.allowNewOrigin;
|
|
1431
|
+
|
|
1432
|
+
const area = ('contentArea' in opt) ? new Rect(opt.contentArea) : this.getContentArea(opt);
|
|
1433
|
+
const { sx, sy } = this.scale();
|
|
1434
|
+
area.x *= sx;
|
|
1435
|
+
area.y *= sy;
|
|
1436
|
+
area.width *= sx;
|
|
1437
|
+
area.height *= sy;
|
|
1438
|
+
|
|
1439
|
+
let calcWidth = Math.ceil((area.width + area.x) / gridWidth);
|
|
1440
|
+
let calcHeight = Math.ceil((area.height + area.y) / gridHeight);
|
|
1441
|
+
if (!opt.allowNegativeBottomRight) {
|
|
1442
|
+
calcWidth = Math.max(calcWidth, 1);
|
|
1443
|
+
calcHeight = Math.max(calcHeight, 1);
|
|
1444
|
+
}
|
|
1445
|
+
calcWidth *= gridWidth;
|
|
1446
|
+
calcHeight *= gridHeight;
|
|
1447
|
+
|
|
1448
|
+
let tx = 0;
|
|
1449
|
+
if ((newOrigin === 'negative' && area.x < 0) || (newOrigin === 'positive' && area.x >= 0) || newOrigin === 'any') {
|
|
1450
|
+
tx = Math.ceil(-area.x / gridWidth) * gridWidth;
|
|
1451
|
+
tx += padding.left;
|
|
1452
|
+
calcWidth += tx;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
let ty = 0;
|
|
1456
|
+
if ((newOrigin === 'negative' && area.y < 0) || (newOrigin === 'positive' && area.y >= 0) || newOrigin === 'any') {
|
|
1457
|
+
ty = Math.ceil(-area.y / gridHeight) * gridHeight;
|
|
1458
|
+
ty += padding.top;
|
|
1459
|
+
calcHeight += ty;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
calcWidth += padding.right;
|
|
1463
|
+
calcHeight += padding.bottom;
|
|
1464
|
+
|
|
1465
|
+
// Make sure the resulting width and height are greater than minimum.
|
|
1466
|
+
calcWidth = Math.max(calcWidth, minWidth);
|
|
1467
|
+
calcHeight = Math.max(calcHeight, minHeight);
|
|
1468
|
+
|
|
1469
|
+
// Make sure the resulting width and height are lesser than maximum.
|
|
1470
|
+
calcWidth = Math.min(calcWidth, maxWidth);
|
|
1471
|
+
calcHeight = Math.min(calcHeight, maxHeight);
|
|
1472
|
+
|
|
1473
|
+
return new Rect(-tx / sx, -ty / sy, calcWidth / sx, calcHeight / sy);
|
|
1474
|
+
},
|
|
1475
|
+
|
|
1476
|
+
transformToFitContent: function(opt) {
|
|
1477
|
+
opt || (opt = {});
|
|
1478
|
+
|
|
1479
|
+
let contentBBox, contentLocalOrigin;
|
|
1480
|
+
if ('contentArea' in opt) {
|
|
1481
|
+
const contentArea = opt.contentArea;
|
|
1482
|
+
contentBBox = this.localToPaperRect(contentArea);
|
|
1483
|
+
contentLocalOrigin = new Point(contentArea);
|
|
1484
|
+
} else {
|
|
1485
|
+
contentBBox = this.getContentBBox(opt);
|
|
1486
|
+
contentLocalOrigin = this.paperToLocalPoint(contentBBox);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (!contentBBox.width || !contentBBox.height) return;
|
|
1490
|
+
|
|
1491
|
+
defaults(opt, {
|
|
1492
|
+
padding: 0,
|
|
1493
|
+
preserveAspectRatio: true,
|
|
1494
|
+
scaleGrid: null,
|
|
1495
|
+
minScale: 0,
|
|
1496
|
+
maxScale: Number.MAX_VALUE,
|
|
1497
|
+
verticalAlign: 'top',
|
|
1498
|
+
horizontalAlign: 'left',
|
|
1499
|
+
//minScaleX
|
|
1500
|
+
//minScaleY
|
|
1501
|
+
//maxScaleX
|
|
1502
|
+
//maxScaleY
|
|
1503
|
+
//fittingBBox
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
const padding = normalizeSides(opt.padding);
|
|
1507
|
+
|
|
1508
|
+
const minScaleX = opt.minScaleX || opt.minScale;
|
|
1509
|
+
const maxScaleX = opt.maxScaleX || opt.maxScale;
|
|
1510
|
+
const minScaleY = opt.minScaleY || opt.minScale;
|
|
1511
|
+
const maxScaleY = opt.maxScaleY || opt.maxScale;
|
|
1512
|
+
|
|
1513
|
+
let fittingBBox;
|
|
1514
|
+
if (opt.fittingBBox) {
|
|
1515
|
+
fittingBBox = opt.fittingBBox;
|
|
1516
|
+
} else {
|
|
1517
|
+
const currentTranslate = this.translate();
|
|
1518
|
+
const computedSize = this.getComputedSize();
|
|
1519
|
+
fittingBBox = {
|
|
1520
|
+
x: currentTranslate.tx,
|
|
1521
|
+
y: currentTranslate.ty,
|
|
1522
|
+
width: computedSize.width,
|
|
1523
|
+
height: computedSize.height
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
fittingBBox = new Rect(fittingBBox).moveAndExpand({
|
|
1528
|
+
x: padding.left,
|
|
1529
|
+
y: padding.top,
|
|
1530
|
+
width: -padding.left - padding.right,
|
|
1531
|
+
height: -padding.top - padding.bottom
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
const ctm = this.matrix();
|
|
1535
|
+
const { a: sx, d: sy, e: tx, f: ty } = ctm;
|
|
1536
|
+
|
|
1537
|
+
let newSx = fittingBBox.width / contentBBox.width * sx;
|
|
1538
|
+
let newSy = fittingBBox.height / contentBBox.height * sy;
|
|
1539
|
+
|
|
1540
|
+
if (opt.preserveAspectRatio) {
|
|
1541
|
+
newSx = newSy = Math.min(newSx, newSy);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// snap scale to a grid
|
|
1545
|
+
if (opt.scaleGrid) {
|
|
1546
|
+
|
|
1547
|
+
const gridSize = opt.scaleGrid;
|
|
1548
|
+
|
|
1549
|
+
newSx = gridSize * Math.floor(newSx / gridSize);
|
|
1550
|
+
newSy = gridSize * Math.floor(newSy / gridSize);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// scale min/max boundaries
|
|
1554
|
+
newSx = Math.min(maxScaleX, Math.max(minScaleX, newSx));
|
|
1555
|
+
newSy = Math.min(maxScaleY, Math.max(minScaleY, newSy));
|
|
1556
|
+
|
|
1557
|
+
const scaleDiff = {
|
|
1558
|
+
x: newSx / sx,
|
|
1559
|
+
y: newSy / sy
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
let newOx = fittingBBox.x - contentLocalOrigin.x * newSx - tx;
|
|
1563
|
+
let newOy = fittingBBox.y - contentLocalOrigin.y * newSy - ty;
|
|
1564
|
+
|
|
1565
|
+
switch (opt.verticalAlign) {
|
|
1566
|
+
case 'middle':
|
|
1567
|
+
newOy = newOy + (fittingBBox.height - contentBBox.height * scaleDiff.y) / 2;
|
|
1568
|
+
break;
|
|
1569
|
+
case 'bottom':
|
|
1570
|
+
newOy = newOy + (fittingBBox.height - contentBBox.height * scaleDiff.y);
|
|
1571
|
+
break;
|
|
1572
|
+
case 'top':
|
|
1573
|
+
default:
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
switch (opt.horizontalAlign) {
|
|
1578
|
+
case 'middle':
|
|
1579
|
+
newOx = newOx + (fittingBBox.width - contentBBox.width * scaleDiff.x) / 2;
|
|
1580
|
+
break;
|
|
1581
|
+
case 'right':
|
|
1582
|
+
newOx = newOx + (fittingBBox.width - contentBBox.width * scaleDiff.x);
|
|
1583
|
+
break;
|
|
1584
|
+
case 'left':
|
|
1585
|
+
default:
|
|
1586
|
+
break;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
ctm.a = newSx;
|
|
1590
|
+
ctm.d = newSy;
|
|
1591
|
+
ctm.e = newOx;
|
|
1592
|
+
ctm.f = newOy;
|
|
1593
|
+
this.matrix(ctm, opt);
|
|
1594
|
+
},
|
|
1595
|
+
|
|
1596
|
+
scaleContentToFit: function(opt) {
|
|
1597
|
+
this.transformToFitContent(opt);
|
|
1598
|
+
},
|
|
1599
|
+
|
|
1600
|
+
// Return the dimensions of the content area in local units (without transformations).
|
|
1601
|
+
getContentArea: function(opt) {
|
|
1602
|
+
|
|
1603
|
+
if (opt && opt.useModelGeometry) {
|
|
1604
|
+
return this.model.getBBox() || new Rect();
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return V(this.cells).getBBox();
|
|
1608
|
+
},
|
|
1609
|
+
|
|
1610
|
+
// Return the dimensions of the content bbox in the paper units (as it appears on screen).
|
|
1611
|
+
getContentBBox: function(opt) {
|
|
1612
|
+
|
|
1613
|
+
return this.localToPaperRect(this.getContentArea(opt));
|
|
1614
|
+
},
|
|
1615
|
+
|
|
1616
|
+
// Returns a geometry rectangle representing the entire
|
|
1617
|
+
// paper area (coordinates from the left paper border to the right one
|
|
1618
|
+
// and the top border to the bottom one).
|
|
1619
|
+
getArea: function() {
|
|
1620
|
+
|
|
1621
|
+
return this.paperToLocalRect(this.getComputedSize());
|
|
1622
|
+
},
|
|
1623
|
+
|
|
1624
|
+
getRestrictedArea: function(...args) {
|
|
1625
|
+
|
|
1626
|
+
const { restrictTranslate } = this.options;
|
|
1627
|
+
|
|
1628
|
+
let restrictedArea;
|
|
1629
|
+
if (isFunction(restrictTranslate)) {
|
|
1630
|
+
// A method returning a bounding box
|
|
1631
|
+
restrictedArea = restrictTranslate.apply(this, args);
|
|
1632
|
+
} else if (restrictTranslate === true) {
|
|
1633
|
+
// The paper area
|
|
1634
|
+
restrictedArea = this.getArea();
|
|
1635
|
+
} else if (!restrictTranslate) {
|
|
1636
|
+
// falsy value
|
|
1637
|
+
restrictedArea = null;
|
|
1638
|
+
} else {
|
|
1639
|
+
// any other value
|
|
1640
|
+
restrictedArea = new Rect(restrictTranslate);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
return restrictedArea;
|
|
1644
|
+
},
|
|
1645
|
+
|
|
1646
|
+
createViewForModel: function(cell) {
|
|
1647
|
+
|
|
1648
|
+
const { options } = this;
|
|
1649
|
+
// A class taken from the paper options.
|
|
1650
|
+
var optionalViewClass;
|
|
1651
|
+
|
|
1652
|
+
// A default basic class (either dia.ElementView or dia.LinkView)
|
|
1653
|
+
var defaultViewClass;
|
|
1654
|
+
|
|
1655
|
+
// A special class defined for this model in the corresponding namespace.
|
|
1656
|
+
// e.g. joint.shapes.standard.Rectangle searches for joint.shapes.standard.RectangleView
|
|
1657
|
+
var namespace = options.cellViewNamespace;
|
|
1658
|
+
var type = cell.get('type') + 'View';
|
|
1659
|
+
var namespaceViewClass = getByPath(namespace, type, '.');
|
|
1660
|
+
|
|
1661
|
+
if (cell.isLink()) {
|
|
1662
|
+
optionalViewClass = options.linkView;
|
|
1663
|
+
defaultViewClass = LinkView;
|
|
1664
|
+
} else {
|
|
1665
|
+
optionalViewClass = options.elementView;
|
|
1666
|
+
defaultViewClass = ElementView;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// a) the paper options view is a class (deprecated)
|
|
1670
|
+
// 1. search the namespace for a view
|
|
1671
|
+
// 2. if no view was found, use view from the paper options
|
|
1672
|
+
// b) the paper options view is a function
|
|
1673
|
+
// 1. call the function from the paper options
|
|
1674
|
+
// 2. if no view was return, search the namespace for a view
|
|
1675
|
+
// 3. if no view was found, use the default
|
|
1676
|
+
var ViewClass = (optionalViewClass.prototype instanceof ViewBase)
|
|
1677
|
+
? namespaceViewClass || optionalViewClass
|
|
1678
|
+
: optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass;
|
|
1679
|
+
|
|
1680
|
+
return new ViewClass({
|
|
1681
|
+
model: cell,
|
|
1682
|
+
interactive: options.interactive,
|
|
1683
|
+
labelsLayer: options.labelsLayer === true ? LayersNames.LABELS : options.labelsLayer
|
|
1684
|
+
});
|
|
1685
|
+
},
|
|
1686
|
+
|
|
1687
|
+
removeView: function(cell) {
|
|
1688
|
+
|
|
1689
|
+
const { id } = cell;
|
|
1690
|
+
const { _views, _updates } = this;
|
|
1691
|
+
const view = _views[id];
|
|
1692
|
+
if (view) {
|
|
1693
|
+
var { cid } = view;
|
|
1694
|
+
const { mounted, unmounted } = _updates;
|
|
1695
|
+
view.remove();
|
|
1696
|
+
delete _views[id];
|
|
1697
|
+
delete mounted[cid];
|
|
1698
|
+
delete unmounted[cid];
|
|
1699
|
+
}
|
|
1700
|
+
return view;
|
|
1701
|
+
},
|
|
1702
|
+
|
|
1703
|
+
renderView: function(cell, opt) {
|
|
1704
|
+
|
|
1705
|
+
const { id } = cell;
|
|
1706
|
+
const views = this._views;
|
|
1707
|
+
let view, flag;
|
|
1708
|
+
let create = true;
|
|
1709
|
+
if (id in views) {
|
|
1710
|
+
view = views[id];
|
|
1711
|
+
if (view.model === cell) {
|
|
1712
|
+
flag = view.FLAG_INSERT;
|
|
1713
|
+
create = false;
|
|
1714
|
+
} else {
|
|
1715
|
+
// The view for this `id` already exist.
|
|
1716
|
+
// The cell is a new instance of the model with identical id
|
|
1717
|
+
// We simply remove the existing view and create a new one
|
|
1718
|
+
this.removeView(cell);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (create) {
|
|
1722
|
+
view = views[id] = this.createViewForModel(cell);
|
|
1723
|
+
view.paper = this;
|
|
1724
|
+
flag = this.registerUnmountedView(view) | this.FLAG_INIT | view.getFlag(result(view, 'initFlag'));
|
|
1725
|
+
}
|
|
1726
|
+
this.requestViewUpdate(view, flag, view.UPDATE_PRIORITY, opt);
|
|
1727
|
+
return view;
|
|
1728
|
+
},
|
|
1729
|
+
|
|
1730
|
+
onImageDragStart: function() {
|
|
1731
|
+
// This is the only way to prevent image dragging in Firefox that works.
|
|
1732
|
+
// Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help.
|
|
1733
|
+
|
|
1734
|
+
return false;
|
|
1735
|
+
},
|
|
1736
|
+
|
|
1737
|
+
resetViews: function(cells, opt) {
|
|
1738
|
+
opt || (opt = {});
|
|
1739
|
+
cells || (cells = []);
|
|
1740
|
+
this._resetUpdates();
|
|
1741
|
+
// clearing views removes any event listeners
|
|
1742
|
+
this.removeViews();
|
|
1743
|
+
// Allows to unfreeze normally while in the idle state using autoFreeze option
|
|
1744
|
+
const key = this.options.autoFreeze ? null : 'reset';
|
|
1745
|
+
this.freeze({ key });
|
|
1746
|
+
for (var i = 0, n = cells.length; i < n; i++) {
|
|
1747
|
+
this.renderView(cells[i], opt);
|
|
1748
|
+
}
|
|
1749
|
+
this.unfreeze({ key });
|
|
1750
|
+
this.sortViews();
|
|
1751
|
+
},
|
|
1752
|
+
|
|
1753
|
+
removeViews: function() {
|
|
1754
|
+
|
|
1755
|
+
invoke(this._views, 'remove');
|
|
1756
|
+
|
|
1757
|
+
this._views = {};
|
|
1758
|
+
},
|
|
1759
|
+
|
|
1760
|
+
sortViews: function() {
|
|
1761
|
+
|
|
1762
|
+
if (!this.isExactSorting()) {
|
|
1763
|
+
// noop
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (this.isFrozen()) {
|
|
1767
|
+
// sort views once unfrozen
|
|
1768
|
+
this._updates.sort = true;
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
this.sortViewsExact();
|
|
1772
|
+
},
|
|
1773
|
+
|
|
1774
|
+
sortViewsExact: function() {
|
|
1775
|
+
|
|
1776
|
+
// Run insertion sort algorithm in order to efficiently sort DOM elements according to their
|
|
1777
|
+
// associated model `z` attribute.
|
|
1778
|
+
|
|
1779
|
+
var cellNodes = Array.from(this.cells.childNodes).filter(node => node.getAttribute('model-id'));
|
|
1780
|
+
var cells = this.model.get('cells');
|
|
1781
|
+
|
|
1782
|
+
sortElements(cellNodes, function(a, b) {
|
|
1783
|
+
var cellA = cells.get(a.getAttribute('model-id'));
|
|
1784
|
+
var cellB = cells.get(b.getAttribute('model-id'));
|
|
1785
|
+
var zA = cellA.attributes.z || 0;
|
|
1786
|
+
var zB = cellB.attributes.z || 0;
|
|
1787
|
+
return (zA === zB) ? 0 : (zA < zB) ? -1 : 1;
|
|
1788
|
+
});
|
|
1789
|
+
},
|
|
1790
|
+
|
|
1791
|
+
insertView: function(view, isInitialInsert) {
|
|
1792
|
+
const layerView = this.getLayerView(LayersNames.CELLS);
|
|
1793
|
+
const { el, model } = view;
|
|
1794
|
+
switch (this.options.sorting) {
|
|
1795
|
+
case sortingTypes.APPROX:
|
|
1796
|
+
layerView.insertSortedNode(el, model.get('z'));
|
|
1797
|
+
break;
|
|
1798
|
+
case sortingTypes.EXACT:
|
|
1799
|
+
default:
|
|
1800
|
+
layerView.insertNode(el);
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
view.onMount(isInitialInsert);
|
|
1804
|
+
},
|
|
1805
|
+
|
|
1806
|
+
detachView(view) {
|
|
1807
|
+
view.unmount();
|
|
1808
|
+
view.onDetach();
|
|
1809
|
+
},
|
|
1810
|
+
|
|
1811
|
+
// Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also
|
|
1812
|
+
// be a selector or a jQuery object.
|
|
1813
|
+
findView: function($el) {
|
|
1814
|
+
|
|
1815
|
+
var el = isString($el)
|
|
1816
|
+
? this.cells.querySelector($el)
|
|
1817
|
+
: $el instanceof $ ? $el[0] : $el;
|
|
1818
|
+
|
|
1819
|
+
var id = this.findAttribute('model-id', el);
|
|
1820
|
+
if (id) return this._views[id];
|
|
1821
|
+
|
|
1822
|
+
return undefined;
|
|
1823
|
+
},
|
|
1824
|
+
|
|
1825
|
+
// Find a view for a model `cell`. `cell` can also be a string or number representing a model `id`.
|
|
1826
|
+
findViewByModel: function(cell) {
|
|
1827
|
+
|
|
1828
|
+
var id = (isString(cell) || isNumber(cell)) ? cell : (cell && cell.id);
|
|
1829
|
+
|
|
1830
|
+
return this._views[id];
|
|
1831
|
+
},
|
|
1832
|
+
|
|
1833
|
+
// Find all views at given point
|
|
1834
|
+
findViewsFromPoint: function(p) {
|
|
1835
|
+
|
|
1836
|
+
p = new Point(p);
|
|
1837
|
+
|
|
1838
|
+
var views = this.model.getElements().map(this.findViewByModel, this);
|
|
1839
|
+
|
|
1840
|
+
return views.filter(function(view) {
|
|
1841
|
+
return view && view.vel.getBBox({ target: this.cells }).containsPoint(p);
|
|
1842
|
+
}, this);
|
|
1843
|
+
},
|
|
1844
|
+
|
|
1845
|
+
// Find all views in given area
|
|
1846
|
+
findViewsInArea: function(rect, opt) {
|
|
1847
|
+
|
|
1848
|
+
opt = defaults(opt || {}, { strict: false });
|
|
1849
|
+
rect = new Rect(rect);
|
|
1850
|
+
|
|
1851
|
+
var views = this.model.getElements().map(this.findViewByModel, this);
|
|
1852
|
+
var method = opt.strict ? 'containsRect' : 'intersect';
|
|
1853
|
+
|
|
1854
|
+
return views.filter(function(view) {
|
|
1855
|
+
return view && rect[method](view.vel.getBBox({ target: this.cells }));
|
|
1856
|
+
}, this);
|
|
1857
|
+
},
|
|
1858
|
+
|
|
1859
|
+
removeTools: function() {
|
|
1860
|
+
this.dispatchToolsEvent('remove');
|
|
1861
|
+
return this;
|
|
1862
|
+
},
|
|
1863
|
+
|
|
1864
|
+
hideTools: function() {
|
|
1865
|
+
this.dispatchToolsEvent('hide');
|
|
1866
|
+
return this;
|
|
1867
|
+
},
|
|
1868
|
+
|
|
1869
|
+
showTools: function() {
|
|
1870
|
+
this.dispatchToolsEvent('show');
|
|
1871
|
+
return this;
|
|
1872
|
+
},
|
|
1873
|
+
|
|
1874
|
+
dispatchToolsEvent: function(event, ...args) {
|
|
1875
|
+
if (typeof event !== 'string') return;
|
|
1876
|
+
this.trigger('tools:event', event, ...args);
|
|
1877
|
+
},
|
|
1878
|
+
|
|
1879
|
+
|
|
1880
|
+
getModelById: function(id) {
|
|
1881
|
+
|
|
1882
|
+
return this.model.getCell(id);
|
|
1883
|
+
},
|
|
1884
|
+
|
|
1885
|
+
snapToGrid: function(x, y) {
|
|
1886
|
+
|
|
1887
|
+
// Convert global coordinates to the local ones of the `viewport`. Otherwise,
|
|
1888
|
+
// improper transformation would be applied when the viewport gets transformed (scaled/rotated).
|
|
1889
|
+
return this.clientToLocalPoint(x, y).snapToGrid(this.options.gridSize);
|
|
1890
|
+
},
|
|
1891
|
+
|
|
1892
|
+
localToPaperPoint: function(x, y) {
|
|
1893
|
+
// allow `x` to be a point and `y` undefined
|
|
1894
|
+
var localPoint = new Point(x, y);
|
|
1895
|
+
var paperPoint = V.transformPoint(localPoint, this.matrix());
|
|
1896
|
+
return paperPoint;
|
|
1897
|
+
},
|
|
1898
|
+
|
|
1899
|
+
localToPaperRect: function(x, y, width, height) {
|
|
1900
|
+
// allow `x` to be a rectangle and rest arguments undefined
|
|
1901
|
+
var localRect = new Rect(x, y, width, height);
|
|
1902
|
+
var paperRect = V.transformRect(localRect, this.matrix());
|
|
1903
|
+
return paperRect;
|
|
1904
|
+
},
|
|
1905
|
+
|
|
1906
|
+
paperToLocalPoint: function(x, y) {
|
|
1907
|
+
// allow `x` to be a point and `y` undefined
|
|
1908
|
+
var paperPoint = new Point(x, y);
|
|
1909
|
+
var localPoint = V.transformPoint(paperPoint, this.matrix().inverse());
|
|
1910
|
+
return localPoint;
|
|
1911
|
+
},
|
|
1912
|
+
|
|
1913
|
+
paperToLocalRect: function(x, y, width, height) {
|
|
1914
|
+
// allow `x` to be a rectangle and rest arguments undefined
|
|
1915
|
+
var paperRect = new Rect(x, y, width, height);
|
|
1916
|
+
var localRect = V.transformRect(paperRect, this.matrix().inverse());
|
|
1917
|
+
return localRect;
|
|
1918
|
+
},
|
|
1919
|
+
|
|
1920
|
+
localToClientPoint: function(x, y) {
|
|
1921
|
+
// allow `x` to be a point and `y` undefined
|
|
1922
|
+
var localPoint = new Point(x, y);
|
|
1923
|
+
var clientPoint = V.transformPoint(localPoint, this.clientMatrix());
|
|
1924
|
+
return clientPoint;
|
|
1925
|
+
},
|
|
1926
|
+
|
|
1927
|
+
localToClientRect: function(x, y, width, height) {
|
|
1928
|
+
// allow `x` to be a point and `y` undefined
|
|
1929
|
+
var localRect = new Rect(x, y, width, height);
|
|
1930
|
+
var clientRect = V.transformRect(localRect, this.clientMatrix());
|
|
1931
|
+
return clientRect;
|
|
1932
|
+
},
|
|
1933
|
+
|
|
1934
|
+
// Transform client coordinates to the paper local coordinates.
|
|
1935
|
+
// Useful when you have a mouse event object and you'd like to get coordinates
|
|
1936
|
+
// inside the paper that correspond to `evt.clientX` and `evt.clientY` point.
|
|
1937
|
+
// Example: var localPoint = paper.clientToLocalPoint({ x: evt.clientX, y: evt.clientY });
|
|
1938
|
+
clientToLocalPoint: function(x, y) {
|
|
1939
|
+
// allow `x` to be a point and `y` undefined
|
|
1940
|
+
var clientPoint = new Point(x, y);
|
|
1941
|
+
var localPoint = V.transformPoint(clientPoint, this.clientMatrix().inverse());
|
|
1942
|
+
return localPoint;
|
|
1943
|
+
},
|
|
1944
|
+
|
|
1945
|
+
clientToLocalRect: function(x, y, width, height) {
|
|
1946
|
+
// allow `x` to be a point and `y` undefined
|
|
1947
|
+
var clientRect = new Rect(x, y, width, height);
|
|
1948
|
+
var localRect = V.transformRect(clientRect, this.clientMatrix().inverse());
|
|
1949
|
+
return localRect;
|
|
1950
|
+
},
|
|
1951
|
+
|
|
1952
|
+
localToPagePoint: function(x, y) {
|
|
1953
|
+
|
|
1954
|
+
return this.localToPaperPoint(x, y).offset(this.pageOffset());
|
|
1955
|
+
},
|
|
1956
|
+
|
|
1957
|
+
localToPageRect: function(x, y, width, height) {
|
|
1958
|
+
|
|
1959
|
+
return this.localToPaperRect(x, y, width, height).offset(this.pageOffset());
|
|
1960
|
+
},
|
|
1961
|
+
|
|
1962
|
+
pageToLocalPoint: function(x, y) {
|
|
1963
|
+
|
|
1964
|
+
var pagePoint = new Point(x, y);
|
|
1965
|
+
var paperPoint = pagePoint.difference(this.pageOffset());
|
|
1966
|
+
return this.paperToLocalPoint(paperPoint);
|
|
1967
|
+
},
|
|
1968
|
+
|
|
1969
|
+
pageToLocalRect: function(x, y, width, height) {
|
|
1970
|
+
|
|
1971
|
+
var pageOffset = this.pageOffset();
|
|
1972
|
+
var paperRect = new Rect(x, y, width, height);
|
|
1973
|
+
paperRect.x -= pageOffset.x;
|
|
1974
|
+
paperRect.y -= pageOffset.y;
|
|
1975
|
+
return this.paperToLocalRect(paperRect);
|
|
1976
|
+
},
|
|
1977
|
+
|
|
1978
|
+
clientOffset: function() {
|
|
1979
|
+
|
|
1980
|
+
var clientRect = this.svg.getBoundingClientRect();
|
|
1981
|
+
return new Point(clientRect.left, clientRect.top);
|
|
1982
|
+
},
|
|
1983
|
+
|
|
1984
|
+
pageOffset: function() {
|
|
1985
|
+
|
|
1986
|
+
return this.clientOffset().offset(window.scrollX, window.scrollY);
|
|
1987
|
+
},
|
|
1988
|
+
|
|
1989
|
+
linkAllowed: function(linkView) {
|
|
1990
|
+
|
|
1991
|
+
if (!(linkView instanceof LinkView)) {
|
|
1992
|
+
throw new Error('Must provide a linkView.');
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
var link = linkView.model;
|
|
1996
|
+
var paperOptions = this.options;
|
|
1997
|
+
var graph = this.model;
|
|
1998
|
+
var ns = graph.constructor.validations;
|
|
1999
|
+
|
|
2000
|
+
if (!paperOptions.multiLinks) {
|
|
2001
|
+
if (!ns.multiLinks.call(this, graph, link)) return false;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
if (!paperOptions.linkPinning) {
|
|
2005
|
+
// Link pinning is not allowed and the link is not connected to the target.
|
|
2006
|
+
if (!ns.linkPinning.call(this, graph, link)) return false;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (typeof paperOptions.allowLink === 'function') {
|
|
2010
|
+
if (!paperOptions.allowLink.call(this, linkView, this)) return false;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
return true;
|
|
2014
|
+
},
|
|
2015
|
+
|
|
2016
|
+
getDefaultLink: function(cellView, magnet) {
|
|
2017
|
+
|
|
2018
|
+
return isFunction(this.options.defaultLink)
|
|
2019
|
+
// default link is a function producing link model
|
|
2020
|
+
? this.options.defaultLink.call(this, cellView, magnet)
|
|
2021
|
+
// default link is the mvc model
|
|
2022
|
+
: this.options.defaultLink.clone();
|
|
2023
|
+
},
|
|
2024
|
+
|
|
2025
|
+
// Cell highlighting.
|
|
2026
|
+
// ------------------
|
|
2027
|
+
|
|
2028
|
+
resolveHighlighter: function(opt = {}) {
|
|
2029
|
+
|
|
2030
|
+
let { highlighter: highlighterDef, type } = opt;
|
|
2031
|
+
const { highlighting,highlighterNamespace } = this.options;
|
|
2032
|
+
|
|
2033
|
+
/*
|
|
2034
|
+
Expecting opt.highlighter to have the following structure:
|
|
2035
|
+
{
|
|
2036
|
+
name: 'highlighter-name',
|
|
2037
|
+
options: {
|
|
2038
|
+
some: 'value'
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
*/
|
|
2042
|
+
if (highlighterDef === undefined) {
|
|
2043
|
+
|
|
2044
|
+
// Is highlighting disabled?
|
|
2045
|
+
if (!highlighting) return false;
|
|
2046
|
+
// check for built-in types
|
|
2047
|
+
if (type) {
|
|
2048
|
+
highlighterDef = highlighting[type];
|
|
2049
|
+
// Is a specific type highlight disabled?
|
|
2050
|
+
if (highlighterDef === false) return false;
|
|
2051
|
+
}
|
|
2052
|
+
if (!highlighterDef) {
|
|
2053
|
+
// Type not defined use default highlight
|
|
2054
|
+
highlighterDef = highlighting['default'];
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// Do nothing if opt.highlighter is falsy.
|
|
2059
|
+
// This allows the case to not highlight cell(s) in certain cases.
|
|
2060
|
+
// For example, if you want to NOT highlight when embedding elements
|
|
2061
|
+
// or use a custom highlighter.
|
|
2062
|
+
if (!highlighterDef) return false;
|
|
2063
|
+
|
|
2064
|
+
// Allow specifying a highlighter by name.
|
|
2065
|
+
if (isString(highlighterDef)) {
|
|
2066
|
+
highlighterDef = {
|
|
2067
|
+
name: highlighterDef
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const name = highlighterDef.name;
|
|
2072
|
+
const highlighter = highlighterNamespace[name];
|
|
2073
|
+
|
|
2074
|
+
// Highlighter validation
|
|
2075
|
+
if (!highlighter) {
|
|
2076
|
+
throw new Error('Unknown highlighter ("' + name + '")');
|
|
2077
|
+
}
|
|
2078
|
+
if (typeof highlighter.highlight !== 'function') {
|
|
2079
|
+
throw new Error('Highlighter ("' + name + '") is missing required highlight() method');
|
|
2080
|
+
}
|
|
2081
|
+
if (typeof highlighter.unhighlight !== 'function') {
|
|
2082
|
+
throw new Error('Highlighter ("' + name + '") is missing required unhighlight() method');
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
return {
|
|
2086
|
+
highlighter,
|
|
2087
|
+
options: highlighterDef.options || {},
|
|
2088
|
+
name
|
|
2089
|
+
};
|
|
2090
|
+
},
|
|
2091
|
+
|
|
2092
|
+
onCellHighlight: function(cellView, magnetEl, opt) {
|
|
2093
|
+
const highlighterDescriptor = this.resolveHighlighter(opt);
|
|
2094
|
+
if (!highlighterDescriptor) return;
|
|
2095
|
+
const { highlighter, options } = highlighterDescriptor;
|
|
2096
|
+
highlighter.highlight(cellView, magnetEl, options);
|
|
2097
|
+
},
|
|
2098
|
+
|
|
2099
|
+
onCellUnhighlight: function(cellView, magnetEl, opt) {
|
|
2100
|
+
const highlighterDescriptor = this.resolveHighlighter(opt);
|
|
2101
|
+
if (!highlighterDescriptor) return;
|
|
2102
|
+
const { highlighter, options } = highlighterDescriptor;
|
|
2103
|
+
highlighter.unhighlight(cellView, magnetEl, options);
|
|
2104
|
+
},
|
|
2105
|
+
|
|
2106
|
+
// Interaction.
|
|
2107
|
+
// ------------
|
|
2108
|
+
|
|
2109
|
+
pointerdblclick: function(evt) {
|
|
2110
|
+
|
|
2111
|
+
evt.preventDefault();
|
|
2112
|
+
|
|
2113
|
+
// magnetpointerdblclick can stop propagation
|
|
2114
|
+
|
|
2115
|
+
evt = normalizeEvent(evt);
|
|
2116
|
+
|
|
2117
|
+
var view = this.findView(evt.target);
|
|
2118
|
+
if (this.guard(evt, view)) return;
|
|
2119
|
+
|
|
2120
|
+
var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
|
|
2121
|
+
|
|
2122
|
+
if (view) {
|
|
2123
|
+
view.pointerdblclick(evt, localPoint.x, localPoint.y);
|
|
2124
|
+
|
|
2125
|
+
} else {
|
|
2126
|
+
this.trigger('blank:pointerdblclick', evt, localPoint.x, localPoint.y);
|
|
2127
|
+
}
|
|
2128
|
+
},
|
|
2129
|
+
|
|
2130
|
+
pointerclick: function(evt) {
|
|
2131
|
+
|
|
2132
|
+
// magnetpointerclick can stop propagation
|
|
2133
|
+
|
|
2134
|
+
var data = this.eventData(evt);
|
|
2135
|
+
// Trigger event only if mouse has not moved.
|
|
2136
|
+
if (data.mousemoved <= this.options.clickThreshold) {
|
|
2137
|
+
|
|
2138
|
+
evt = normalizeEvent(evt);
|
|
2139
|
+
|
|
2140
|
+
var view = this.findView(evt.target);
|
|
2141
|
+
if (this.guard(evt, view)) return;
|
|
2142
|
+
|
|
2143
|
+
var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
|
|
2144
|
+
|
|
2145
|
+
if (view) {
|
|
2146
|
+
view.pointerclick(evt, localPoint.x, localPoint.y);
|
|
2147
|
+
|
|
2148
|
+
} else {
|
|
2149
|
+
this.trigger('blank:pointerclick', evt, localPoint.x, localPoint.y);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
},
|
|
2153
|
+
|
|
2154
|
+
contextmenu: function(evt) {
|
|
2155
|
+
|
|
2156
|
+
if (this.options.preventContextMenu) evt.preventDefault();
|
|
2157
|
+
|
|
2158
|
+
if (this.contextMenuFired) {
|
|
2159
|
+
this.contextMenuFired = false;
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
evt = normalizeEvent(evt);
|
|
2164
|
+
|
|
2165
|
+
this.contextMenuTrigger(evt);
|
|
2166
|
+
},
|
|
2167
|
+
|
|
2168
|
+
contextMenuTrigger: function(evt) {
|
|
2169
|
+
var view = this.findView(evt.target);
|
|
2170
|
+
if (this.guard(evt, view)) return;
|
|
2171
|
+
|
|
2172
|
+
var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
|
|
2173
|
+
|
|
2174
|
+
if (view) {
|
|
2175
|
+
view.contextmenu(evt, localPoint.x, localPoint.y);
|
|
2176
|
+
|
|
2177
|
+
} else {
|
|
2178
|
+
this.trigger('blank:contextmenu', evt, localPoint.x, localPoint.y);
|
|
2179
|
+
}
|
|
2180
|
+
},
|
|
2181
|
+
|
|
2182
|
+
pointerdown: function(evt) {
|
|
2183
|
+
|
|
2184
|
+
evt = normalizeEvent(evt);
|
|
2185
|
+
|
|
2186
|
+
const { target, button } = evt;
|
|
2187
|
+
const view = this.findView(target);
|
|
2188
|
+
const isContextMenu = (button === 2);
|
|
2189
|
+
|
|
2190
|
+
if (view) {
|
|
2191
|
+
|
|
2192
|
+
if (!isContextMenu && this.guard(evt, view)) return;
|
|
2193
|
+
|
|
2194
|
+
const isTargetFormNode = this.FORM_CONTROL_TAG_NAMES.includes(target.tagName);
|
|
2195
|
+
|
|
2196
|
+
if (this.options.preventDefaultViewAction && !isTargetFormNode) {
|
|
2197
|
+
// If the target is a form element, we do not want to prevent the default action.
|
|
2198
|
+
// For example, we want to be able to select text in a text input or
|
|
2199
|
+
// to be able to click on a checkbox.
|
|
2200
|
+
evt.preventDefault();
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
if (isTargetFormNode) {
|
|
2204
|
+
// If the target is a form element, we do not want to start dragging the element.
|
|
2205
|
+
// For example, we want to be able to select text by dragging the mouse.
|
|
2206
|
+
view.preventDefaultInteraction(evt);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// Custom event
|
|
2210
|
+
const eventEvt = this.customEventTrigger(evt, view);
|
|
2211
|
+
if (eventEvt) {
|
|
2212
|
+
// `onevent` could have stopped propagation
|
|
2213
|
+
if (eventEvt.isPropagationStopped()) return;
|
|
2214
|
+
|
|
2215
|
+
evt.data = eventEvt.data;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// Element magnet
|
|
2219
|
+
const magnetNode = target.closest('[magnet]');
|
|
2220
|
+
if (magnetNode && view.el !== magnetNode && view.el.contains(magnetNode)) {
|
|
2221
|
+
const magnetEvt = normalizeEvent(new $.Event(evt.originalEvent, {
|
|
2222
|
+
data: evt.data,
|
|
2223
|
+
// Originally the event listener was attached to the magnet element.
|
|
2224
|
+
currentTarget: magnetNode
|
|
2225
|
+
}));
|
|
2226
|
+
this.onmagnet(magnetEvt);
|
|
2227
|
+
if (magnetEvt.isDefaultPrevented()) {
|
|
2228
|
+
evt.preventDefault();
|
|
2229
|
+
}
|
|
2230
|
+
// `onmagnet` stops propagation when `addLinkFromMagnet` is allowed
|
|
2231
|
+
if (magnetEvt.isPropagationStopped()) {
|
|
2232
|
+
// `magnet:pointermove` and `magnet:pointerup` events must be fired
|
|
2233
|
+
if (isContextMenu) return;
|
|
2234
|
+
this.delegateDragEvents(view, magnetEvt.data);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
evt.data = magnetEvt.data;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
if (isContextMenu) {
|
|
2242
|
+
this.contextMenuFired = true;
|
|
2243
|
+
const contextmenuEvt = new $.Event(evt.originalEvent, { type: 'contextmenu', data: evt.data });
|
|
2244
|
+
this.contextMenuTrigger(contextmenuEvt);
|
|
2245
|
+
} else {
|
|
2246
|
+
const localPoint = this.snapToGrid(evt.clientX, evt.clientY);
|
|
2247
|
+
if (view) {
|
|
2248
|
+
view.pointerdown(evt, localPoint.x, localPoint.y);
|
|
2249
|
+
} else {
|
|
2250
|
+
if (this.options.preventDefaultBlankAction) {
|
|
2251
|
+
evt.preventDefault();
|
|
2252
|
+
}
|
|
2253
|
+
this.trigger('blank:pointerdown', evt, localPoint.x, localPoint.y);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
this.delegateDragEvents(view, evt.data);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
},
|
|
2260
|
+
|
|
2261
|
+
pointermove: function(evt) {
|
|
2262
|
+
|
|
2263
|
+
// mouse moved counter
|
|
2264
|
+
var data = this.eventData(evt);
|
|
2265
|
+
if (!data.mousemoved) {
|
|
2266
|
+
data.mousemoved = 0;
|
|
2267
|
+
// Make sure that events like `mouseenter` and `mouseleave` are
|
|
2268
|
+
// not triggered while the user is dragging a cellView.
|
|
2269
|
+
this.undelegateEvents();
|
|
2270
|
+
// Note: the events are undelegated after the first `pointermove` event.
|
|
2271
|
+
// Not on `pointerdown` to make sure that `dbltap` is recognized.
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
var mousemoved = ++data.mousemoved;
|
|
2275
|
+
|
|
2276
|
+
if (mousemoved <= this.options.moveThreshold) return;
|
|
2277
|
+
|
|
2278
|
+
evt = normalizeEvent(evt);
|
|
2279
|
+
|
|
2280
|
+
var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
|
|
2281
|
+
|
|
2282
|
+
var view = data.sourceView;
|
|
2283
|
+
if (view) {
|
|
2284
|
+
view.pointermove(evt, localPoint.x, localPoint.y);
|
|
2285
|
+
} else {
|
|
2286
|
+
this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
this.eventData(evt, data);
|
|
2290
|
+
},
|
|
2291
|
+
|
|
2292
|
+
pointerup: function(evt) {
|
|
2293
|
+
|
|
2294
|
+
this.undelegateDocumentEvents();
|
|
2295
|
+
|
|
2296
|
+
var normalizedEvt = normalizeEvent(evt);
|
|
2297
|
+
|
|
2298
|
+
var localPoint = this.snapToGrid(normalizedEvt.clientX, normalizedEvt.clientY);
|
|
2299
|
+
|
|
2300
|
+
var view = this.eventData(evt).sourceView;
|
|
2301
|
+
if (view) {
|
|
2302
|
+
view.pointerup(normalizedEvt, localPoint.x, localPoint.y);
|
|
2303
|
+
} else {
|
|
2304
|
+
this.trigger('blank:pointerup', normalizedEvt, localPoint.x, localPoint.y);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
if (!normalizedEvt.isPropagationStopped()) {
|
|
2308
|
+
this.pointerclick(new $.Event(evt.originalEvent, { type: 'click', data: evt.data }));
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
this.delegateEvents();
|
|
2312
|
+
},
|
|
2313
|
+
|
|
2314
|
+
mouseover: function(evt) {
|
|
2315
|
+
|
|
2316
|
+
evt = normalizeEvent(evt);
|
|
2317
|
+
|
|
2318
|
+
var view = this.findView(evt.target);
|
|
2319
|
+
if (this.guard(evt, view)) return;
|
|
2320
|
+
|
|
2321
|
+
if (view) {
|
|
2322
|
+
view.mouseover(evt);
|
|
2323
|
+
|
|
2324
|
+
} else {
|
|
2325
|
+
if (this.el === evt.target) return; // prevent border of paper from triggering this
|
|
2326
|
+
this.trigger('blank:mouseover', evt);
|
|
2327
|
+
}
|
|
2328
|
+
},
|
|
2329
|
+
|
|
2330
|
+
mouseout: function(evt) {
|
|
2331
|
+
|
|
2332
|
+
evt = normalizeEvent(evt);
|
|
2333
|
+
|
|
2334
|
+
var view = this.findView(evt.target);
|
|
2335
|
+
if (this.guard(evt, view)) return;
|
|
2336
|
+
|
|
2337
|
+
if (view) {
|
|
2338
|
+
view.mouseout(evt);
|
|
2339
|
+
|
|
2340
|
+
} else {
|
|
2341
|
+
if (this.el === evt.target) return; // prevent border of paper from triggering this
|
|
2342
|
+
this.trigger('blank:mouseout', evt);
|
|
2343
|
+
}
|
|
2344
|
+
},
|
|
2345
|
+
|
|
2346
|
+
mouseenter: function(evt) {
|
|
2347
|
+
|
|
2348
|
+
evt = normalizeEvent(evt);
|
|
2349
|
+
|
|
2350
|
+
const {
|
|
2351
|
+
target, // The EventTarget the pointing device entered to
|
|
2352
|
+
relatedTarget, // The EventTarget the pointing device exited from
|
|
2353
|
+
currentTarget // The EventTarget on which the event listener was registered
|
|
2354
|
+
} = evt;
|
|
2355
|
+
const view = this.findView(target);
|
|
2356
|
+
if (this.guard(evt, view)) return;
|
|
2357
|
+
const relatedView = this.findView(relatedTarget);
|
|
2358
|
+
if (view) {
|
|
2359
|
+
if (relatedView === view) {
|
|
2360
|
+
// Mouse left a cell tool
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
view.mouseenter(evt);
|
|
2364
|
+
if (this.el.contains(relatedTarget)) {
|
|
2365
|
+
// The pointer remains inside the paper.
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
if (relatedView) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
// prevent double `mouseenter` event if the `relatedTarget` is outside the paper
|
|
2373
|
+
// (mouseenter method would be fired twice)
|
|
2374
|
+
if (currentTarget === this.el) {
|
|
2375
|
+
// `paper` (more descriptive), not `blank`
|
|
2376
|
+
this.trigger('paper:mouseenter', evt);
|
|
2377
|
+
}
|
|
2378
|
+
},
|
|
2379
|
+
|
|
2380
|
+
mouseleave: function(evt) {
|
|
2381
|
+
|
|
2382
|
+
evt = normalizeEvent(evt);
|
|
2383
|
+
|
|
2384
|
+
const {
|
|
2385
|
+
target, // The EventTarget the pointing device exited from
|
|
2386
|
+
relatedTarget, // The EventTarget the pointing device entered to
|
|
2387
|
+
currentTarget // The EventTarget on which the event listener was registered
|
|
2388
|
+
} = evt;
|
|
2389
|
+
const view = this.findView(target);
|
|
2390
|
+
if (this.guard(evt, view)) return;
|
|
2391
|
+
const relatedView = this.findView(relatedTarget);
|
|
2392
|
+
if (view) {
|
|
2393
|
+
if (relatedView === view) {
|
|
2394
|
+
// Mouse entered a cell tool
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
view.mouseleave(evt);
|
|
2398
|
+
if (this.el.contains(relatedTarget)) {
|
|
2399
|
+
// The pointer has exited a cellView. The pointer is still inside of the paper.
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
if (relatedView) {
|
|
2404
|
+
// The pointer has entered a new cellView
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
// prevent double `mouseleave` event if the `relatedTarget` is outside the paper
|
|
2408
|
+
// (mouseleave method would be fired twice)
|
|
2409
|
+
if (currentTarget === this.el) {
|
|
2410
|
+
// There is no cellView under the pointer, nor the blank area of the paper
|
|
2411
|
+
this.trigger('paper:mouseleave', evt);
|
|
2412
|
+
}
|
|
2413
|
+
},
|
|
2414
|
+
|
|
2415
|
+
_processMouseWheelEvtBuf: debounce(function() {
|
|
2416
|
+
const { event, deltas } = this._mw_evt_buffer;
|
|
2417
|
+
const deltaY = deltas.reduce((acc, deltaY) => acc + cap(deltaY, WHEEL_CAP), 0);
|
|
2418
|
+
|
|
2419
|
+
const scale = Math.pow(0.995, deltaY); // 1.005 for inverted pinch/zoom
|
|
2420
|
+
const { x, y } = this.clientToLocalPoint(event.clientX, event.clientY);
|
|
2421
|
+
this.trigger('paper:pinch', event, x, y, scale);
|
|
2422
|
+
|
|
2423
|
+
this._mw_evt_buffer = {
|
|
2424
|
+
event: null,
|
|
2425
|
+
deltas: [],
|
|
2426
|
+
};
|
|
2427
|
+
}, WHEEL_WAIT_MS, { maxWait: WHEEL_WAIT_MS }),
|
|
2428
|
+
|
|
2429
|
+
mousewheel: function(evt) {
|
|
2430
|
+
|
|
2431
|
+
evt = normalizeEvent(evt);
|
|
2432
|
+
|
|
2433
|
+
const view = this.findView(evt.target);
|
|
2434
|
+
if (this.guard(evt, view)) return;
|
|
2435
|
+
|
|
2436
|
+
const originalEvent = evt.originalEvent;
|
|
2437
|
+
const localPoint = this.snapToGrid(originalEvent.clientX, originalEvent.clientY);
|
|
2438
|
+
const { deltaX, deltaY } = normalizeWheel(originalEvent);
|
|
2439
|
+
|
|
2440
|
+
const pinchHandlers = this._events['paper:pinch'];
|
|
2441
|
+
|
|
2442
|
+
// Touchpad devices will send a fake CTRL press when a pinch is performed
|
|
2443
|
+
//
|
|
2444
|
+
// We also check if there are any subscribers to paper:pinch event. If there are none,
|
|
2445
|
+
// just skip the entire block of code (we don't want to blindly call
|
|
2446
|
+
// .preventDefault() if we really don't have to).
|
|
2447
|
+
if (evt.ctrlKey && pinchHandlers && pinchHandlers.length > 0) {
|
|
2448
|
+
// This is a pinch gesture, it's safe to assume that we must call .preventDefault()
|
|
2449
|
+
originalEvent.preventDefault();
|
|
2450
|
+
this._mw_evt_buffer.event = evt;
|
|
2451
|
+
this._mw_evt_buffer.deltas.push(deltaY);
|
|
2452
|
+
this._processMouseWheelEvtBuf();
|
|
2453
|
+
} else {
|
|
2454
|
+
const delta = Math.max(-1, Math.min(1, originalEvent.wheelDelta));
|
|
2455
|
+
if (view) {
|
|
2456
|
+
view.mousewheel(evt, localPoint.x, localPoint.y, delta);
|
|
2457
|
+
|
|
2458
|
+
} else {
|
|
2459
|
+
this.trigger('blank:mousewheel', evt, localPoint.x, localPoint.y, delta);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
this.trigger('paper:pan', evt, deltaX, deltaY);
|
|
2463
|
+
}
|
|
2464
|
+
},
|
|
2465
|
+
|
|
2466
|
+
onevent: function(evt) {
|
|
2467
|
+
|
|
2468
|
+
var eventNode = evt.currentTarget;
|
|
2469
|
+
var eventName = eventNode.getAttribute('event');
|
|
2470
|
+
if (eventName) {
|
|
2471
|
+
var view = this.findView(eventNode);
|
|
2472
|
+
if (view) {
|
|
2473
|
+
|
|
2474
|
+
evt = normalizeEvent(evt);
|
|
2475
|
+
if (this.guard(evt, view)) return;
|
|
2476
|
+
|
|
2477
|
+
var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
|
|
2478
|
+
view.onevent(evt, eventName, localPoint.x, localPoint.y);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
},
|
|
2482
|
+
|
|
2483
|
+
magnetEvent: function(evt, handler) {
|
|
2484
|
+
|
|
2485
|
+
var magnetNode = evt.currentTarget;
|
|
2486
|
+
var magnetValue = magnetNode.getAttribute('magnet');
|
|
2487
|
+
if (magnetValue) {
|
|
2488
|
+
var view = this.findView(magnetNode);
|
|
2489
|
+
if (view) {
|
|
2490
|
+
evt = normalizeEvent(evt);
|
|
2491
|
+
if (this.guard(evt, view)) return;
|
|
2492
|
+
var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
|
|
2493
|
+
handler.call(this, view, evt, magnetNode, localPoint.x, localPoint.y);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
},
|
|
2497
|
+
|
|
2498
|
+
onmagnet: function(evt) {
|
|
2499
|
+
|
|
2500
|
+
if (evt.button === 2) {
|
|
2501
|
+
this.contextMenuFired = true;
|
|
2502
|
+
this.magnetContextMenuFired = true;
|
|
2503
|
+
const contextmenuEvt = new $.Event(evt.originalEvent, {
|
|
2504
|
+
type: 'contextmenu',
|
|
2505
|
+
data: evt.data,
|
|
2506
|
+
currentTarget: evt.currentTarget,
|
|
2507
|
+
});
|
|
2508
|
+
this.magnetContextMenuTrigger(contextmenuEvt);
|
|
2509
|
+
if (contextmenuEvt.isPropagationStopped()) {
|
|
2510
|
+
evt.stopPropagation();
|
|
2511
|
+
}
|
|
2512
|
+
} else {
|
|
2513
|
+
this.magnetEvent(evt, function(view, evt, _, x, y) {
|
|
2514
|
+
view.onmagnet(evt, x, y);
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
},
|
|
2518
|
+
|
|
2519
|
+
magnetpointerdblclick: function(evt) {
|
|
2520
|
+
|
|
2521
|
+
this.magnetEvent(evt, function(view, evt, magnet, x, y) {
|
|
2522
|
+
view.magnetpointerdblclick(evt, magnet, x, y);
|
|
2523
|
+
});
|
|
2524
|
+
},
|
|
2525
|
+
|
|
2526
|
+
magnetcontextmenu: function(evt) {
|
|
2527
|
+
if (this.options.preventContextMenu) evt.preventDefault();
|
|
2528
|
+
|
|
2529
|
+
if (this.magnetContextMenuFired) {
|
|
2530
|
+
this.magnetContextMenuFired = false;
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
this.magnetContextMenuTrigger(evt);
|
|
2535
|
+
},
|
|
2536
|
+
|
|
2537
|
+
magnetContextMenuTrigger: function(evt) {
|
|
2538
|
+
this.magnetEvent(evt, function(view, evt, magnet, x, y) {
|
|
2539
|
+
view.magnetcontextmenu(evt, magnet, x, y);
|
|
2540
|
+
});
|
|
2541
|
+
},
|
|
2542
|
+
|
|
2543
|
+
onlabel: function(evt) {
|
|
2544
|
+
|
|
2545
|
+
var labelNode = evt.currentTarget;
|
|
2546
|
+
|
|
2547
|
+
var view = this.findView(labelNode);
|
|
2548
|
+
if (!view) return;
|
|
2549
|
+
|
|
2550
|
+
evt = normalizeEvent(evt);
|
|
2551
|
+
if (this.guard(evt, view)) return;
|
|
2552
|
+
|
|
2553
|
+
// Custom event
|
|
2554
|
+
const eventEvt = this.customEventTrigger(evt, view, labelNode);
|
|
2555
|
+
if (eventEvt) {
|
|
2556
|
+
// `onevent` could have stopped propagation
|
|
2557
|
+
if (eventEvt.isPropagationStopped()) return;
|
|
2558
|
+
|
|
2559
|
+
evt.data = eventEvt.data;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
|
|
2563
|
+
view.onlabel(evt, localPoint.x, localPoint.y);
|
|
2564
|
+
},
|
|
2565
|
+
|
|
2566
|
+
getPointerArgs(evt) {
|
|
2567
|
+
const normalizedEvt = normalizeEvent(evt);
|
|
2568
|
+
const { x, y } = this.snapToGrid(normalizedEvt.clientX, normalizedEvt.clientY);
|
|
2569
|
+
return [normalizedEvt, x, y];
|
|
2570
|
+
},
|
|
2571
|
+
|
|
2572
|
+
delegateDragEvents: function(view, data) {
|
|
2573
|
+
|
|
2574
|
+
data || (data = {});
|
|
2575
|
+
this.eventData({ data: data }, { sourceView: view || null, mousemoved: 0 });
|
|
2576
|
+
this.delegateDocumentEvents(null, data);
|
|
2577
|
+
},
|
|
2578
|
+
|
|
2579
|
+
// Guard the specified event. If the event should be ignored, guard returns `true`.
|
|
2580
|
+
// Otherwise, it returns `false`.
|
|
2581
|
+
guard: function(evt, view) {
|
|
2582
|
+
|
|
2583
|
+
if (evt.type === 'mousedown' && evt.button === 2) {
|
|
2584
|
+
// handled as `contextmenu` type
|
|
2585
|
+
return true;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
if (this.options.guard && this.options.guard(evt, view)) {
|
|
2589
|
+
return true;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
if (evt.data && evt.data.guarded !== undefined) {
|
|
2593
|
+
return evt.data.guarded;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
const { target } = evt;
|
|
2597
|
+
|
|
2598
|
+
if (this.GUARDED_TAG_NAMES.includes(target.tagName)) {
|
|
2599
|
+
return true;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
if (view && view.model && (view.model instanceof Cell)) {
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
if (this.el === target || this.svg.contains(target)) {
|
|
2607
|
+
return false;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
return true; // Event guarded. Paper should not react on it in any way.
|
|
2611
|
+
},
|
|
2612
|
+
|
|
2613
|
+
setGridSize: function(gridSize) {
|
|
2614
|
+
const { options } = this;
|
|
2615
|
+
options.gridSize = gridSize;
|
|
2616
|
+
if (options.drawGrid && !options.drawGridSize) {
|
|
2617
|
+
// Do not redraw the grid if the `drawGridSize` is set.
|
|
2618
|
+
this.getLayerView(LayersNames.GRID).renderGrid();
|
|
2619
|
+
}
|
|
2620
|
+
return this;
|
|
2621
|
+
},
|
|
2622
|
+
|
|
2623
|
+
setGrid: function(drawGrid) {
|
|
2624
|
+
this.getLayerView(LayersNames.GRID).setGrid(drawGrid);
|
|
2625
|
+
return this;
|
|
2626
|
+
},
|
|
2627
|
+
|
|
2628
|
+
updateBackgroundImage: function(opt) {
|
|
2629
|
+
|
|
2630
|
+
opt = opt || {};
|
|
2631
|
+
|
|
2632
|
+
var backgroundPosition = opt.position || 'center';
|
|
2633
|
+
var backgroundSize = opt.size || 'auto auto';
|
|
2634
|
+
|
|
2635
|
+
var currentScale = this.scale();
|
|
2636
|
+
var currentTranslate = this.translate();
|
|
2637
|
+
|
|
2638
|
+
// backgroundPosition
|
|
2639
|
+
if (isObject(backgroundPosition)) {
|
|
2640
|
+
var x = currentTranslate.tx + (currentScale.sx * (backgroundPosition.x || 0));
|
|
2641
|
+
var y = currentTranslate.ty + (currentScale.sy * (backgroundPosition.y || 0));
|
|
2642
|
+
backgroundPosition = x + 'px ' + y + 'px';
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// backgroundSize
|
|
2646
|
+
if (isObject(backgroundSize)) {
|
|
2647
|
+
backgroundSize = new Rect(backgroundSize).scale(currentScale.sx, currentScale.sy);
|
|
2648
|
+
backgroundSize = backgroundSize.width + 'px ' + backgroundSize.height + 'px';
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
const { background } = this.childNodes;
|
|
2652
|
+
background.style.backgroundSize = backgroundSize;
|
|
2653
|
+
background.style.backgroundPosition = backgroundPosition;
|
|
2654
|
+
},
|
|
2655
|
+
|
|
2656
|
+
drawBackgroundImage: function(img, opt) {
|
|
2657
|
+
|
|
2658
|
+
// Clear the background image if no image provided
|
|
2659
|
+
if (!(img instanceof HTMLImageElement)) {
|
|
2660
|
+
this.childNodes.background.style.backgroundImage = '';
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
if (!this._background || this._background.id !== opt.id) {
|
|
2665
|
+
// Draw only the last image requested (see drawBackground())
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
opt = opt || {};
|
|
2670
|
+
|
|
2671
|
+
var backgroundImage;
|
|
2672
|
+
var backgroundSize = opt.size;
|
|
2673
|
+
var backgroundRepeat = opt.repeat || 'no-repeat';
|
|
2674
|
+
var backgroundOpacity = opt.opacity || 1;
|
|
2675
|
+
var backgroundQuality = Math.abs(opt.quality) || 1;
|
|
2676
|
+
var backgroundPattern = this.constructor.backgroundPatterns[camelCase(backgroundRepeat)];
|
|
2677
|
+
|
|
2678
|
+
if (isFunction(backgroundPattern)) {
|
|
2679
|
+
// 'flip-x', 'flip-y', 'flip-xy', 'watermark' and custom
|
|
2680
|
+
img.width *= backgroundQuality;
|
|
2681
|
+
img.height *= backgroundQuality;
|
|
2682
|
+
var canvas = backgroundPattern(img, opt);
|
|
2683
|
+
if (!(canvas instanceof HTMLCanvasElement)) {
|
|
2684
|
+
throw new Error('dia.Paper: background pattern must return an HTML Canvas instance');
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
backgroundImage = canvas.toDataURL('image/png');
|
|
2688
|
+
backgroundRepeat = 'repeat';
|
|
2689
|
+
if (isObject(backgroundSize)) {
|
|
2690
|
+
// recalculate the tile size if an object passed in
|
|
2691
|
+
backgroundSize.width *= canvas.width / img.width;
|
|
2692
|
+
backgroundSize.height *= canvas.height / img.height;
|
|
2693
|
+
} else if (backgroundSize === undefined) {
|
|
2694
|
+
// calculate the tile size if no provided
|
|
2695
|
+
opt.size = {
|
|
2696
|
+
width: canvas.width / backgroundQuality,
|
|
2697
|
+
height: canvas.height / backgroundQuality
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
} else {
|
|
2701
|
+
// backgroundRepeat:
|
|
2702
|
+
// no-repeat', 'round', 'space', 'repeat', 'repeat-x', 'repeat-y'
|
|
2703
|
+
backgroundImage = img.src;
|
|
2704
|
+
if (backgroundSize === undefined) {
|
|
2705
|
+
// pass the image size for the backgroundSize if no size provided
|
|
2706
|
+
opt.size = {
|
|
2707
|
+
width: img.width,
|
|
2708
|
+
height: img.height
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
this.childNodes.background.style.opacity = backgroundOpacity;
|
|
2714
|
+
this.childNodes.background.style.backgroundRepeat = backgroundRepeat;
|
|
2715
|
+
this.childNodes.background.style.backgroundImage = `url(${backgroundImage})`;
|
|
2716
|
+
|
|
2717
|
+
this.updateBackgroundImage(opt);
|
|
2718
|
+
},
|
|
2719
|
+
|
|
2720
|
+
updateBackgroundColor: function(color) {
|
|
2721
|
+
|
|
2722
|
+
this.$el.css('backgroundColor', color || '');
|
|
2723
|
+
},
|
|
2724
|
+
|
|
2725
|
+
drawBackground: function(opt) {
|
|
2726
|
+
|
|
2727
|
+
opt = opt || {};
|
|
2728
|
+
|
|
2729
|
+
this.updateBackgroundColor(opt.color);
|
|
2730
|
+
|
|
2731
|
+
if (opt.image) {
|
|
2732
|
+
opt = this._background = cloneDeep(opt);
|
|
2733
|
+
guid(opt);
|
|
2734
|
+
var img = document.createElement('img');
|
|
2735
|
+
img.onload = this.drawBackgroundImage.bind(this, img, opt);
|
|
2736
|
+
img.src = opt.image;
|
|
2737
|
+
} else {
|
|
2738
|
+
this.drawBackgroundImage(null);
|
|
2739
|
+
this._background = null;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
return this;
|
|
2743
|
+
},
|
|
2744
|
+
|
|
2745
|
+
setInteractivity: function(value) {
|
|
2746
|
+
|
|
2747
|
+
this.options.interactive = value;
|
|
2748
|
+
|
|
2749
|
+
invoke(this._views, 'setInteractivity', value);
|
|
2750
|
+
},
|
|
2751
|
+
|
|
2752
|
+
// Paper definitions.
|
|
2753
|
+
// ------------------
|
|
2754
|
+
|
|
2755
|
+
isDefined: function(defId) {
|
|
2756
|
+
|
|
2757
|
+
return !!this.svg.getElementById(defId);
|
|
2758
|
+
},
|
|
2759
|
+
|
|
2760
|
+
defineFilter: function(filter) {
|
|
2761
|
+
|
|
2762
|
+
if (!isObject(filter)) {
|
|
2763
|
+
throw new TypeError('dia.Paper: defineFilter() requires 1. argument to be an object.');
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
var filterId = filter.id;
|
|
2767
|
+
var name = filter.name;
|
|
2768
|
+
// Generate a hash code from the stringified filter definition. This gives us
|
|
2769
|
+
// a unique filter ID for different definitions.
|
|
2770
|
+
if (!filterId) {
|
|
2771
|
+
filterId = name + this.svg.id + hashCode(JSON.stringify(filter));
|
|
2772
|
+
}
|
|
2773
|
+
// If the filter already exists in the document,
|
|
2774
|
+
// we're done and we can just use it (reference it using `url()`).
|
|
2775
|
+
// If not, create one.
|
|
2776
|
+
if (!this.isDefined(filterId)) {
|
|
2777
|
+
|
|
2778
|
+
var namespace = _filter;
|
|
2779
|
+
var filterSVGString = namespace[name] && namespace[name](filter.args || {});
|
|
2780
|
+
if (!filterSVGString) {
|
|
2781
|
+
throw new Error('Non-existing filter ' + name);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// SVG <filter/> attributes
|
|
2785
|
+
var filterAttrs = assign({
|
|
2786
|
+
filterUnits: 'userSpaceOnUse',
|
|
2787
|
+
}, filter.attrs, {
|
|
2788
|
+
id: filterId
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
V(filterSVGString, filterAttrs).appendTo(this.defs);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
return filterId;
|
|
2795
|
+
},
|
|
2796
|
+
|
|
2797
|
+
defineGradient: function(gradient) {
|
|
2798
|
+
if (!isObject(gradient)) {
|
|
2799
|
+
throw new TypeError('dia.Paper: defineGradient() requires 1. argument to be an object.');
|
|
2800
|
+
}
|
|
2801
|
+
const { svg, defs } = this;
|
|
2802
|
+
const {
|
|
2803
|
+
type,
|
|
2804
|
+
// Generate a hash code from the stringified filter definition. This gives us
|
|
2805
|
+
// a unique filter ID for different definitions.
|
|
2806
|
+
id = type + svg.id + hashCode(JSON.stringify(gradient)),
|
|
2807
|
+
stops,
|
|
2808
|
+
attrs = {}
|
|
2809
|
+
} = gradient;
|
|
2810
|
+
// If the gradient already exists in the document,
|
|
2811
|
+
// we're done and we can just use it (reference it using `url()`).
|
|
2812
|
+
if (this.isDefined(id)) return id;
|
|
2813
|
+
// If not, create one.
|
|
2814
|
+
const stopVEls = toArray(stops).map(({ offset, color, opacity }) => {
|
|
2815
|
+
return V('stop').attr({
|
|
2816
|
+
'offset': offset,
|
|
2817
|
+
'stop-color': color,
|
|
2818
|
+
'stop-opacity': Number.isFinite(opacity) ? opacity : 1
|
|
2819
|
+
});
|
|
2820
|
+
});
|
|
2821
|
+
const gradientVEl = V(type, attrs, stopVEls);
|
|
2822
|
+
gradientVEl.id = id;
|
|
2823
|
+
gradientVEl.appendTo(defs);
|
|
2824
|
+
return id;
|
|
2825
|
+
},
|
|
2826
|
+
|
|
2827
|
+
definePattern: function(pattern) {
|
|
2828
|
+
if (!isObject(pattern)) {
|
|
2829
|
+
throw new TypeError('dia.Paper: definePattern() requires 1. argument to be an object.');
|
|
2830
|
+
}
|
|
2831
|
+
const { svg, defs } = this;
|
|
2832
|
+
const {
|
|
2833
|
+
// Generate a hash code from the stringified filter definition. This gives us
|
|
2834
|
+
// a unique filter ID for different definitions.
|
|
2835
|
+
id = svg.id + hashCode(JSON.stringify(pattern)),
|
|
2836
|
+
markup,
|
|
2837
|
+
attrs = {}
|
|
2838
|
+
} = pattern;
|
|
2839
|
+
if (!markup) {
|
|
2840
|
+
throw new TypeError('dia.Paper: definePattern() requires markup.');
|
|
2841
|
+
}
|
|
2842
|
+
// If the gradient already exists in the document,
|
|
2843
|
+
// we're done and we can just use it (reference it using `url()`).
|
|
2844
|
+
if (this.isDefined(id)) return id;
|
|
2845
|
+
// If not, create one.
|
|
2846
|
+
const patternVEl = V('pattern', {
|
|
2847
|
+
patternUnits: 'userSpaceOnUse'
|
|
2848
|
+
});
|
|
2849
|
+
patternVEl.id = id;
|
|
2850
|
+
patternVEl.attr(attrs);
|
|
2851
|
+
if (typeof markup === 'string') {
|
|
2852
|
+
patternVEl.append(V(markup));
|
|
2853
|
+
} else {
|
|
2854
|
+
const { fragment } = parseDOMJSON(markup);
|
|
2855
|
+
patternVEl.append(fragment);
|
|
2856
|
+
}
|
|
2857
|
+
patternVEl.appendTo(defs);
|
|
2858
|
+
return id;
|
|
2859
|
+
},
|
|
2860
|
+
|
|
2861
|
+
defineMarker: function(marker) {
|
|
2862
|
+
if (!isObject(marker)) {
|
|
2863
|
+
throw new TypeError('dia.Paper: defineMarker() requires the first argument to be an object.');
|
|
2864
|
+
}
|
|
2865
|
+
const { svg, defs } = this;
|
|
2866
|
+
const {
|
|
2867
|
+
// Generate a hash code from the stringified filter definition. This gives us
|
|
2868
|
+
// a unique filter ID for different definitions.
|
|
2869
|
+
id = svg.id + hashCode(JSON.stringify(marker)),
|
|
2870
|
+
// user-provided markup
|
|
2871
|
+
// (e.g. defined when creating link via `attrs/line/sourceMarker/markup`)
|
|
2872
|
+
markup,
|
|
2873
|
+
// user-provided attributes
|
|
2874
|
+
// (e.g. defined when creating link via `attrs/line/sourceMarker/attrs`)
|
|
2875
|
+
// note: `transform` attrs are ignored by browsers
|
|
2876
|
+
attrs = {},
|
|
2877
|
+
// deprecated - use `attrs/markerUnits` instead (which has higher priority)
|
|
2878
|
+
markerUnits = 'userSpaceOnUse'
|
|
2879
|
+
} = marker;
|
|
2880
|
+
// If the marker already exists in the document,
|
|
2881
|
+
// we're done and we can just use it (reference it using `url()`).
|
|
2882
|
+
if (this.isDefined(id)) return id;
|
|
2883
|
+
// If not, create one.
|
|
2884
|
+
const markerVEl = V('marker', {
|
|
2885
|
+
orient: 'auto',
|
|
2886
|
+
overflow: 'visible',
|
|
2887
|
+
markerUnits: markerUnits
|
|
2888
|
+
});
|
|
2889
|
+
markerVEl.id = id;
|
|
2890
|
+
markerVEl.attr(attrs);
|
|
2891
|
+
let markerContentVEl;
|
|
2892
|
+
if (markup) {
|
|
2893
|
+
let markupVEl;
|
|
2894
|
+
if (typeof markup === 'string') {
|
|
2895
|
+
// Marker object has a `markup` property of type string.
|
|
2896
|
+
// - Construct V from the provided string.
|
|
2897
|
+
markupVEl = V(markup);
|
|
2898
|
+
// `markupVEl` is now either a single VEl, or an array of VEls.
|
|
2899
|
+
// - Coerce it to an array.
|
|
2900
|
+
markupVEl = (Array.isArray(markupVEl) ? markupVEl : [markupVEl]);
|
|
2901
|
+
} else {
|
|
2902
|
+
// Marker object has a `markup` property of type object.
|
|
2903
|
+
// - Construct V from the object by parsing it as DOM JSON.
|
|
2904
|
+
const { fragment } = parseDOMJSON(markup);
|
|
2905
|
+
markupVEl = V(fragment).children();
|
|
2906
|
+
}
|
|
2907
|
+
// `markupVEl` is an array with one or more VEls inside.
|
|
2908
|
+
// - If there are multiple VEls, wrap them in a newly-constructed <g> element
|
|
2909
|
+
if (markupVEl.length > 1) {
|
|
2910
|
+
markerContentVEl = V('g').append(markupVEl);
|
|
2911
|
+
} else {
|
|
2912
|
+
markerContentVEl = markupVEl[0];
|
|
2913
|
+
}
|
|
2914
|
+
} else {
|
|
2915
|
+
// Marker object is a flat structure.
|
|
2916
|
+
// - Construct a new V of type `marker.type`.
|
|
2917
|
+
const { type = 'path' } = marker;
|
|
2918
|
+
markerContentVEl = V(type);
|
|
2919
|
+
}
|
|
2920
|
+
// `markerContentVEl` is a single VEl.
|
|
2921
|
+
// Assign additional attributes to it (= context attributes + marker attributes):
|
|
2922
|
+
// - Attribute values are taken from non-special properties of `marker`.
|
|
2923
|
+
const markerAttrs = omit(marker, 'type', 'id', 'markup', 'attrs', 'markerUnits');
|
|
2924
|
+
const markerAttrsKeys = Object.keys(markerAttrs);
|
|
2925
|
+
markerAttrsKeys.forEach((key) => {
|
|
2926
|
+
const value = markerAttrs[key];
|
|
2927
|
+
const markupValue = markerContentVEl.attr(key); // value coming from markupVEl (if any) = higher priority
|
|
2928
|
+
if (markupValue == null) {
|
|
2929
|
+
// Default logic:
|
|
2930
|
+
markerContentVEl.attr(key, value);
|
|
2931
|
+
} else {
|
|
2932
|
+
// Properties with special logic should be added as cases to this switch block:
|
|
2933
|
+
switch(key) {
|
|
2934
|
+
case 'transform':
|
|
2935
|
+
// - Prepend `transform` to existing value.
|
|
2936
|
+
markerContentVEl.attr(key, (value + ' ' + markupValue));
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
markerContentVEl.appendTo(markerVEl);
|
|
2942
|
+
markerVEl.appendTo(defs);
|
|
2943
|
+
return id;
|
|
2944
|
+
},
|
|
2945
|
+
|
|
2946
|
+
customEventTrigger: function(evt, view, rootNode = view.el) {
|
|
2947
|
+
|
|
2948
|
+
const eventNode = evt.target.closest('[event]');
|
|
2949
|
+
|
|
2950
|
+
if (eventNode && rootNode !== eventNode && view.el.contains(eventNode)) {
|
|
2951
|
+
const eventEvt = normalizeEvent(new $.Event(evt.originalEvent, {
|
|
2952
|
+
data: evt.data,
|
|
2953
|
+
// Originally the event listener was attached to the event element.
|
|
2954
|
+
currentTarget: eventNode
|
|
2955
|
+
}));
|
|
2956
|
+
|
|
2957
|
+
this.onevent(eventEvt);
|
|
2958
|
+
|
|
2959
|
+
if (eventEvt.isDefaultPrevented()) {
|
|
2960
|
+
evt.preventDefault();
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
return eventEvt;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
return null;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
}, {
|
|
2970
|
+
|
|
2971
|
+
sorting: sortingTypes,
|
|
2972
|
+
|
|
2973
|
+
Layers: LayersNames,
|
|
2974
|
+
|
|
2975
|
+
backgroundPatterns: {
|
|
2976
|
+
|
|
2977
|
+
flipXy: function(img) {
|
|
2978
|
+
// d b
|
|
2979
|
+
// q p
|
|
2980
|
+
|
|
2981
|
+
var canvas = document.createElement('canvas');
|
|
2982
|
+
var imgWidth = img.width;
|
|
2983
|
+
var imgHeight = img.height;
|
|
2984
|
+
|
|
2985
|
+
canvas.width = 2 * imgWidth;
|
|
2986
|
+
canvas.height = 2 * imgHeight;
|
|
2987
|
+
|
|
2988
|
+
var ctx = canvas.getContext('2d');
|
|
2989
|
+
// top-left image
|
|
2990
|
+
ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
|
|
2991
|
+
// xy-flipped bottom-right image
|
|
2992
|
+
ctx.setTransform(-1, 0, 0, -1, canvas.width, canvas.height);
|
|
2993
|
+
ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
|
|
2994
|
+
// x-flipped top-right image
|
|
2995
|
+
ctx.setTransform(-1, 0, 0, 1, canvas.width, 0);
|
|
2996
|
+
ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
|
|
2997
|
+
// y-flipped bottom-left image
|
|
2998
|
+
ctx.setTransform(1, 0, 0, -1, 0, canvas.height);
|
|
2999
|
+
ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
|
|
3000
|
+
|
|
3001
|
+
return canvas;
|
|
3002
|
+
},
|
|
3003
|
+
|
|
3004
|
+
flipX: function(img) {
|
|
3005
|
+
// d b
|
|
3006
|
+
// d b
|
|
3007
|
+
|
|
3008
|
+
var canvas = document.createElement('canvas');
|
|
3009
|
+
var imgWidth = img.width;
|
|
3010
|
+
var imgHeight = img.height;
|
|
3011
|
+
|
|
3012
|
+
canvas.width = imgWidth * 2;
|
|
3013
|
+
canvas.height = imgHeight;
|
|
3014
|
+
|
|
3015
|
+
var ctx = canvas.getContext('2d');
|
|
3016
|
+
// left image
|
|
3017
|
+
ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
|
|
3018
|
+
// flipped right image
|
|
3019
|
+
ctx.translate(2 * imgWidth, 0);
|
|
3020
|
+
ctx.scale(-1, 1);
|
|
3021
|
+
ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
|
|
3022
|
+
|
|
3023
|
+
return canvas;
|
|
3024
|
+
},
|
|
3025
|
+
|
|
3026
|
+
flipY: function(img) {
|
|
3027
|
+
// d d
|
|
3028
|
+
// q q
|
|
3029
|
+
|
|
3030
|
+
var canvas = document.createElement('canvas');
|
|
3031
|
+
var imgWidth = img.width;
|
|
3032
|
+
var imgHeight = img.height;
|
|
3033
|
+
|
|
3034
|
+
canvas.width = imgWidth;
|
|
3035
|
+
canvas.height = imgHeight * 2;
|
|
3036
|
+
|
|
3037
|
+
var ctx = canvas.getContext('2d');
|
|
3038
|
+
// top image
|
|
3039
|
+
ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
|
|
3040
|
+
// flipped bottom image
|
|
3041
|
+
ctx.translate(0, 2 * imgHeight);
|
|
3042
|
+
ctx.scale(1, -1);
|
|
3043
|
+
ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
|
|
3044
|
+
|
|
3045
|
+
return canvas;
|
|
3046
|
+
},
|
|
3047
|
+
|
|
3048
|
+
watermark: function(img, opt) {
|
|
3049
|
+
// d
|
|
3050
|
+
// d
|
|
3051
|
+
|
|
3052
|
+
opt = opt || {};
|
|
3053
|
+
|
|
3054
|
+
var imgWidth = img.width;
|
|
3055
|
+
var imgHeight = img.height;
|
|
3056
|
+
|
|
3057
|
+
var canvas = document.createElement('canvas');
|
|
3058
|
+
canvas.width = imgWidth * 3;
|
|
3059
|
+
canvas.height = imgHeight * 3;
|
|
3060
|
+
|
|
3061
|
+
var ctx = canvas.getContext('2d');
|
|
3062
|
+
var angle = isNumber(opt.watermarkAngle) ? -opt.watermarkAngle : -20;
|
|
3063
|
+
var radians = toRad(angle);
|
|
3064
|
+
var stepX = canvas.width / 4;
|
|
3065
|
+
var stepY = canvas.height / 4;
|
|
3066
|
+
|
|
3067
|
+
for (var i = 0; i < 4; i++) {
|
|
3068
|
+
for (var j = 0; j < 4; j++) {
|
|
3069
|
+
if ((i + j) % 2 > 0) {
|
|
3070
|
+
// reset the current transformations
|
|
3071
|
+
ctx.setTransform(1, 0, 0, 1, (2 * i - 1) * stepX, (2 * j - 1) * stepY);
|
|
3072
|
+
ctx.rotate(radians);
|
|
3073
|
+
ctx.drawImage(img, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
return canvas;
|
|
3079
|
+
}
|
|
3080
|
+
},
|
|
3081
|
+
|
|
3082
|
+
gridPatterns: {
|
|
3083
|
+
dot: [{
|
|
3084
|
+
color: '#AAAAAA',
|
|
3085
|
+
thickness: 1,
|
|
3086
|
+
markup: 'rect',
|
|
3087
|
+
render: function(el, opt) {
|
|
3088
|
+
V(el).attr({
|
|
3089
|
+
width: opt.thickness,
|
|
3090
|
+
height: opt.thickness,
|
|
3091
|
+
fill: opt.color
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
}],
|
|
3095
|
+
fixedDot: [{
|
|
3096
|
+
color: '#AAAAAA',
|
|
3097
|
+
thickness: 1,
|
|
3098
|
+
markup: 'rect',
|
|
3099
|
+
render: function(el, opt) {
|
|
3100
|
+
V(el).attr({ fill: opt.color });
|
|
3101
|
+
},
|
|
3102
|
+
update: function(el, opt, paper) {
|
|
3103
|
+
const { sx, sy } = paper.scale();
|
|
3104
|
+
const width = sx <= 1 ? opt.thickness : opt.thickness / sx;
|
|
3105
|
+
const height = sy <= 1 ? opt.thickness : opt.thickness / sy;
|
|
3106
|
+
V(el).attr({ width, height });
|
|
3107
|
+
}
|
|
3108
|
+
}],
|
|
3109
|
+
mesh: [{
|
|
3110
|
+
color: '#AAAAAA',
|
|
3111
|
+
thickness: 1,
|
|
3112
|
+
markup: 'path',
|
|
3113
|
+
render: function(el, opt) {
|
|
3114
|
+
|
|
3115
|
+
var d;
|
|
3116
|
+
var width = opt.width;
|
|
3117
|
+
var height = opt.height;
|
|
3118
|
+
var thickness = opt.thickness;
|
|
3119
|
+
|
|
3120
|
+
if (width - thickness >= 0 && height - thickness >= 0) {
|
|
3121
|
+
d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
|
|
3122
|
+
} else {
|
|
3123
|
+
d = 'M 0 0 0 0';
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
|
|
3127
|
+
}
|
|
3128
|
+
}],
|
|
3129
|
+
doubleMesh: [{
|
|
3130
|
+
color: '#AAAAAA',
|
|
3131
|
+
thickness: 1,
|
|
3132
|
+
markup: 'path',
|
|
3133
|
+
render: function(el, opt) {
|
|
3134
|
+
|
|
3135
|
+
var d;
|
|
3136
|
+
var width = opt.width;
|
|
3137
|
+
var height = opt.height;
|
|
3138
|
+
var thickness = opt.thickness;
|
|
3139
|
+
|
|
3140
|
+
if (width - thickness >= 0 && height - thickness >= 0) {
|
|
3141
|
+
d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
|
|
3142
|
+
} else {
|
|
3143
|
+
d = 'M 0 0 0 0';
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
|
|
3147
|
+
}
|
|
3148
|
+
}, {
|
|
3149
|
+
color: '#000000',
|
|
3150
|
+
thickness: 3,
|
|
3151
|
+
scaleFactor: 4,
|
|
3152
|
+
markup: 'path',
|
|
3153
|
+
render: function(el, opt) {
|
|
3154
|
+
|
|
3155
|
+
var d;
|
|
3156
|
+
var width = opt.width;
|
|
3157
|
+
var height = opt.height;
|
|
3158
|
+
var thickness = opt.thickness;
|
|
3159
|
+
|
|
3160
|
+
if (width - thickness >= 0 && height - thickness >= 0) {
|
|
3161
|
+
d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
|
|
3162
|
+
} else {
|
|
3163
|
+
d = 'M 0 0 0 0';
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
|
|
3167
|
+
}
|
|
3168
|
+
}]
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
3171
|
+
|