UNPKG

21.8 kBJavaScriptView Raw
1/*
2* Copyright (C) 1998-2021 by Northwoods Software Corporation. All Rights Reserved.
3*/
4/*
5* This is an extension and not part of the main GoJS library.
6* Note that the API for this class may change with any version, even point releases.
7* If you intend to use an extension in production, you should copy the code to your own source directory.
8* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
9* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
10*/
11import * as go from '../release/go-module.js';
12// These are the definitions for all of the predefined buttons.
13// You do not need to load this file in order to use buttons.
14// A 'Button' is a Panel that has a Shape surrounding some content
15// and that has mouseEnter/mouseLeave behavior to highlight the button.
16// The content of the button, whether a TextBlock or a Picture or a complicated Panel,
17// must be supplied by the caller.
18// The caller must also provide a click event handler.
19// Typical usage:
20// $('Button',
21// $(go.TextBlock, 'Click me!'), // the content is just the text label
22// { click: function(e, obj) { alert('I was clicked'); } }
23// )
24// Note that a button click event handler is not invoked upon a click if isEnabledObject() returns false.
25go.GraphObject.defineBuilder('Button', (args) => {
26 // default colors for 'Button' shape
27 const buttonFillNormal = '#F5F5F5';
28 const buttonStrokeNormal = '#BDBDBD';
29 const buttonFillOver = '#E0E0E0';
30 const buttonStrokeOver = '#9E9E9E';
31 const buttonFillPressed = '#BDBDBD'; // set to null for no button pressed effects
32 const buttonStrokePressed = '#9E9E9E';
33 const buttonFillDisabled = '#E5E5E5';
34 // padding inside the ButtonBorder to match sizing from previous versions
35 const paddingHorizontal = 2.76142374915397;
36 const paddingVertical = 2.761423749153969;
37 const button = /** @type {Panel} */ (go.GraphObject.make(go.Panel, 'Auto', {
38 isActionable: true,
39 enabledChanged: (btn, enabled) => {
40 if (btn instanceof go.Panel) {
41 const shape = btn.findObject('ButtonBorder');
42 if (shape !== null) {
43 shape.fill = enabled ? btn['_buttonFillNormal'] : btn['_buttonFillDisabled'];
44 }
45 }
46 },
47 cursor: 'pointer',
48 // save these values for the mouseEnter and mouseLeave event handlers
49 '_buttonFillNormal': buttonFillNormal,
50 '_buttonStrokeNormal': buttonStrokeNormal,
51 '_buttonFillOver': buttonFillOver,
52 '_buttonStrokeOver': buttonStrokeOver,
53 '_buttonFillPressed': buttonFillPressed,
54 '_buttonStrokePressed': buttonStrokePressed,
55 '_buttonFillDisabled': buttonFillDisabled
56 }, go.GraphObject.make(go.Shape, // the border
57 {
58 name: 'ButtonBorder',
59 figure: 'RoundedRectangle',
60 spot1: new go.Spot(0, 0, paddingHorizontal, paddingVertical),
61 spot2: new go.Spot(1, 1, -paddingHorizontal, -paddingVertical),
62 parameter1: 2,
63 parameter2: 2,
64 fill: buttonFillNormal,
65 stroke: buttonStrokeNormal
66 })));
67 // There's no GraphObject inside the button shape -- it must be added as part of the button definition.
68 // This way the object could be a TextBlock or a Shape or a Picture or arbitrarily complex Panel.
69 // mouse-over behavior
70 button.mouseEnter = (e, btn, prev) => {
71 if (!btn.isEnabledObject())
72 return;
73 if (!(btn instanceof go.Panel))
74 return;
75 const shape = btn.findObject('ButtonBorder'); // the border Shape
76 if (shape instanceof go.Shape) {
77 let brush = btn['_buttonFillOver'];
78 btn['_buttonFillNormal'] = shape.fill;
79 shape.fill = brush;
80 brush = btn['_buttonStrokeOver'];
81 btn['_buttonStrokeNormal'] = shape.stroke;
82 shape.stroke = brush;
83 }
84 };
85 button.mouseLeave = (e, btn, prev) => {
86 if (!btn.isEnabledObject())
87 return;
88 if (!(btn instanceof go.Panel))
89 return;
90 const shape = btn.findObject('ButtonBorder'); // the border Shape
91 if (shape instanceof go.Shape) {
92 shape.fill = btn['_buttonFillNormal'];
93 shape.stroke = btn['_buttonStrokeNormal'];
94 }
95 };
96 button.actionDown = (e, btn) => {
97 if (!btn.isEnabledObject())
98 return;
99 if (!(btn instanceof go.Panel))
100 return;
101 if (btn['_buttonFillPressed'] === null)
102 return;
103 if (e.button !== 0)
104 return;
105 const shape = btn.findObject('ButtonBorder'); // the border Shape
106 if (shape instanceof go.Shape) {
107 const diagram = e.diagram;
108 const oldskip = diagram.skipsUndoManager;
109 diagram.skipsUndoManager = true;
110 let brush = btn['_buttonFillPressed'];
111 btn['_buttonFillOver'] = shape.fill;
112 shape.fill = brush;
113 brush = btn['_buttonStrokePressed'];
114 btn['_buttonStrokeOver'] = shape.stroke;
115 shape.stroke = brush;
116 diagram.skipsUndoManager = oldskip;
117 }
118 };
119 button.actionUp = (e, btn) => {
120 if (!btn.isEnabledObject())
121 return;
122 if (!(btn instanceof go.Panel))
123 return;
124 if (btn['_buttonFillPressed'] === null)
125 return;
126 if (e.button !== 0)
127 return;
128 const shape = btn.findObject('ButtonBorder'); // the border Shape
129 if (shape instanceof go.Shape) {
130 const diagram = e.diagram;
131 const oldskip = diagram.skipsUndoManager;
132 diagram.skipsUndoManager = true;
133 if (overButton(e, btn)) {
134 shape.fill = btn['_buttonFillOver'];
135 shape.stroke = btn['_buttonStrokeOver'];
136 }
137 else {
138 shape.fill = btn['_buttonFillNormal'];
139 shape.stroke = btn['_buttonStrokeNormal'];
140 }
141 diagram.skipsUndoManager = oldskip;
142 }
143 };
144 button.actionCancel = (e, btn) => {
145 if (!btn.isEnabledObject())
146 return;
147 if (!(btn instanceof go.Panel))
148 return;
149 if (btn['_buttonFillPressed'] === null)
150 return;
151 const shape = btn.findObject('ButtonBorder'); // the border Shape
152 if (shape instanceof go.Shape) {
153 const diagram = e.diagram;
154 const oldskip = diagram.skipsUndoManager;
155 diagram.skipsUndoManager = true;
156 if (overButton(e, btn)) {
157 shape.fill = btn['_buttonFillOver'];
158 shape.stroke = btn['_buttonStrokeOver'];
159 }
160 else {
161 shape.fill = btn['_buttonFillNormal'];
162 shape.stroke = btn['_buttonStrokeNormal'];
163 }
164 diagram.skipsUndoManager = oldskip;
165 }
166 };
167 button.actionMove = (e, btn) => {
168 if (!btn.isEnabledObject())
169 return;
170 if (!(btn instanceof go.Panel))
171 return;
172 if (btn['_buttonFillPressed'] === null)
173 return;
174 const diagram = e.diagram;
175 if (diagram.firstInput.button !== 0)
176 return;
177 diagram.currentTool.standardMouseOver();
178 if (overButton(e, btn)) {
179 const shape = btn.findObject('ButtonBorder');
180 if (shape instanceof go.Shape) {
181 const oldskip = diagram.skipsUndoManager;
182 diagram.skipsUndoManager = true;
183 let brush = btn['_buttonFillPressed'];
184 if (shape.fill !== brush)
185 shape.fill = brush;
186 brush = btn['_buttonStrokePressed'];
187 if (shape.stroke !== brush)
188 shape.stroke = brush;
189 diagram.skipsUndoManager = oldskip;
190 }
191 }
192 };
193 const overButton = (e, btn) => {
194 const over = e.diagram.findObjectAt(e.documentPoint, (x) => {
195 while (x.panel !== null) {
196 if (x.isActionable)
197 return x;
198 x = x.panel;
199 }
200 return x;
201 }, (x) => x === btn);
202 return over !== null;
203 };
204 return button;
205});
206// This is a complete Button that you can have in a Node template
207// to allow the user to collapse/expand the subtree beginning at that Node.
208// Typical usage within a Node template:
209// $('TreeExpanderButton')
210go.GraphObject.defineBuilder('TreeExpanderButton', (args) => {
211 const button = /** @type {Panel} */ (go.GraphObject.make('Button', {
212 '_treeExpandedFigure': 'MinusLine',
213 '_treeCollapsedFigure': 'PlusLine'
214 }, go.GraphObject.make(go.Shape, // the icon
215 {
216 name: 'ButtonIcon',
217 figure: 'MinusLine',
218 stroke: '#424242',
219 strokeWidth: 2,
220 desiredSize: new go.Size(8, 8)
221 },
222 // bind the Shape.figure to the Node.isTreeExpanded value using this converter:
223 new go.Binding('figure', 'isTreeExpanded', (exp, shape) => {
224 const but = shape.panel;
225 return exp ? but['_treeExpandedFigure'] : but['_treeCollapsedFigure'];
226 }).ofObject()),
227 // assume initially not visible because there are no links coming out
228 { visible: false },
229 // bind the button visibility to whether it's not a leaf node
230 new go.Binding('visible', 'isTreeLeaf', (leaf) => !leaf).ofObject()));
231 // tree expand/collapse behavior
232 button.click = (e, btn) => {
233 let node = btn.part;
234 if (node instanceof go.Adornment)
235 node = node.adornedPart;
236 if (!(node instanceof go.Node))
237 return;
238 const diagram = node.diagram;
239 if (diagram === null)
240 return;
241 const cmd = diagram.commandHandler;
242 if (node.isTreeExpanded) {
243 if (!cmd.canCollapseTree(node))
244 return;
245 }
246 else {
247 if (!cmd.canExpandTree(node))
248 return;
249 }
250 e.handled = true;
251 if (node.isTreeExpanded) {
252 cmd.collapseTree(node);
253 }
254 else {
255 cmd.expandTree(node);
256 }
257 };
258 return button;
259});
260// This is a complete Button that you can have in a Group template
261// to allow the user to collapse/expand the subgraph that the Group holds.
262// Typical usage within a Group template:
263// $('SubGraphExpanderButton')
264go.GraphObject.defineBuilder('SubGraphExpanderButton', (args) => {
265 const button = /** @type {Panel} */ (go.GraphObject.make('Button', {
266 '_subGraphExpandedFigure': 'MinusLine',
267 '_subGraphCollapsedFigure': 'PlusLine'
268 }, go.GraphObject.make(go.Shape, // the icon
269 {
270 name: 'ButtonIcon',
271 figure: 'MinusLine',
272 stroke: '#424242',
273 strokeWidth: 2,
274 desiredSize: new go.Size(8, 8)
275 },
276 // bind the Shape.figure to the Group.isSubGraphExpanded value using this converter:
277 new go.Binding('figure', 'isSubGraphExpanded', (exp, shape) => {
278 const but = shape.panel;
279 return exp ? but['_subGraphExpandedFigure'] : but['_subGraphCollapsedFigure'];
280 }).ofObject())));
281 // subgraph expand/collapse behavior
282 button.click = (e, btn) => {
283 let group = btn.part;
284 if (group instanceof go.Adornment)
285 group = group.adornedPart;
286 if (!(group instanceof go.Group))
287 return;
288 const diagram = group.diagram;
289 if (diagram === null)
290 return;
291 const cmd = diagram.commandHandler;
292 if (group.isSubGraphExpanded) {
293 if (!cmd.canCollapseSubGraph(group))
294 return;
295 }
296 else {
297 if (!cmd.canExpandSubGraph(group))
298 return;
299 }
300 e.handled = true;
301 if (group.isSubGraphExpanded) {
302 cmd.collapseSubGraph(group);
303 }
304 else {
305 cmd.expandSubGraph(group);
306 }
307 };
308 return button;
309});
310// This is just an "Auto" Adornment that can hold some contents within a light gray, shadowed box.
311// Typical usage:
312// toolTip:
313// $("ToolTip",
314// $(go.TextBlock, . . .)
315// )
316go.GraphObject.defineBuilder('ToolTip', (args) => {
317 const ad = go.GraphObject.make(go.Adornment, 'Auto', {
318 isShadowed: true,
319 shadowColor: 'rgba(0, 0, 0, .4)',
320 shadowOffset: new go.Point(0, 3),
321 shadowBlur: 5
322 }, go.GraphObject.make(go.Shape, {
323 name: 'Border',
324 figure: 'RoundedRectangle',
325 parameter1: 1,
326 parameter2: 1,
327 fill: '#F5F5F5',
328 stroke: '#F0F0F0',
329 spot1: new go.Spot(0, 0, 4, 6),
330 spot2: new go.Spot(1, 1, -4, -4)
331 }));
332 return ad;
333});
334// This is just a "Vertical" Adornment that can hold some "ContextMenuButton"s.
335// Typical usage:
336// contextMenu:
337// $("ContextMenu",
338// $("ContextMenuButton",
339// $(go.TextBlock, . . .),
340// { click: . . .}
341// ),
342// $("ContextMenuButton", . . .)
343// )
344go.GraphObject.defineBuilder('ContextMenu', (args) => {
345 const ad = go.GraphObject.make(go.Adornment, 'Vertical', {
346 background: '#F5F5F5',
347 isShadowed: true,
348 shadowColor: 'rgba(0, 0, 0, .4)',
349 shadowOffset: new go.Point(0, 3),
350 shadowBlur: 5
351 },
352 // don't set the background if the ContextMenu is adorning something and there's a Placeholder
353 new go.Binding('background', '', (obj) => {
354 const part = obj.adornedPart;
355 if (part !== null && obj.placeholder !== null)
356 return null;
357 return '#F5F5F5';
358 }));
359 return ad;
360});
361// This just holds the 'ButtonBorder' Shape that acts as the border
362// around the button contents, which must be supplied by the caller.
363// The button contents are usually a TextBlock or Panel consisting of a Shape and a TextBlock.
364// Typical usage within an Adornment that is either a GraphObject.contextMenu or a Diagram.contextMenu:
365// $('ContextMenuButton',
366// $(go.TextBlock, text),
367// { click: function(e, obj) { alert('Command for ' + obj.part.adornedPart); } },
368// new go.Binding('visible', '', function(data) { return ...OK to perform Command...; })
369// )
370go.GraphObject.defineBuilder('ContextMenuButton', (args) => {
371 const button = /** @type {Panel} */ (go.GraphObject.make('Button'));
372 button.stretch = go.GraphObject.Horizontal;
373 const border = button.findObject('ButtonBorder');
374 if (border instanceof go.Shape) {
375 border.figure = 'Rectangle';
376 border.spot1 = new go.Spot(0, 0, 2, 3);
377 border.spot2 = new go.Spot(1, 1, -2, -2);
378 }
379 return button;
380});
381// This button is used to toggle the visibility of a GraphObject named
382// by the second argument to GraphObject.make. If the second argument is not present
383// or if it is not a string, this assumes that the element name is 'COLLAPSIBLE'.
384// You can only control the visibility of one element in a Part at a time,
385// although that element might be an arbitrarily complex Panel.
386// Typical usage:
387// $(go.Panel, . . .,
388// $('PanelExpanderButton', 'COLLAPSIBLE'),
389// . . .,
390// $(go.Panel, . . .,
391// { name: 'COLLAPSIBLE' },
392// . . . stuff to be hidden or shown as the PanelExpanderButton is clicked . . .
393// ),
394// . . .
395// )
396go.GraphObject.defineBuilder('PanelExpanderButton', (args) => {
397 const eltname = (go.GraphObject.takeBuilderArgument(args, 'COLLAPSIBLE'));
398 const button = (go.GraphObject.make('Button', {
399 '_buttonExpandedFigure': 'M0 0 M0 6 L4 2 8 6 M8 8',
400 '_buttonCollapsedFigure': 'M0 0 M0 2 L4 6 8 2 M8 8',
401 '_buttonFillNormal': 'rgba(0, 0, 0, 0)',
402 '_buttonStrokeNormal': null,
403 '_buttonFillOver': 'rgba(0, 0, 0, .2)',
404 '_buttonStrokeOver': null,
405 '_buttonFillPressed': 'rgba(0, 0, 0, .4)',
406 '_buttonStrokePressed': null
407 }, go.GraphObject.make(go.Shape, { name: 'ButtonIcon', strokeWidth: 2 }, new go.Binding('geometryString', 'visible', (vis) => vis ? button['_buttonExpandedFigure'] : button['_buttonCollapsedFigure']).ofObject(eltname))));
408 const border = button.findObject('ButtonBorder');
409 if (border instanceof go.Shape) {
410 border.stroke = null;
411 border.fill = 'rgba(0, 0, 0, 0)';
412 }
413 button.click = (e, btn) => {
414 if (!(btn instanceof go.Panel))
415 return;
416 const diagram = btn.diagram;
417 if (diagram === null)
418 return;
419 if (diagram.isReadOnly)
420 return;
421 let elt = btn.findTemplateBinder();
422 if (elt === null)
423 elt = btn.part;
424 if (elt !== null) {
425 const pan = elt.findObject(eltname);
426 if (pan !== null) {
427 e.handled = true;
428 diagram.startTransaction('Collapse/Expand Panel');
429 pan.visible = !pan.visible;
430 diagram.commitTransaction('Collapse/Expand Panel');
431 }
432 }
433 };
434 return button;
435});
436// Define a common checkbox button; the first argument is the name of the data property
437// to which the state of this checkbox is data bound. If the first argument is not a string,
438// it raises an error. If no data binding of the checked state is desired,
439// pass an empty string as the first argument.
440// Examples:
441// $('CheckBoxButton', 'dataPropertyName', ...)
442// or:
443// $('CheckBoxButton', '', { '_doClick': function(e, obj) { alert('clicked!'); } })
444go.GraphObject.defineBuilder('CheckBoxButton', (args) => {
445 // process the one required string argument for this kind of button
446 const propname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args));
447 const button = /** @type {Panel} */ (go.GraphObject.make('Button', { desiredSize: new go.Size(14, 14) }, go.GraphObject.make(go.Shape, {
448 name: 'ButtonIcon',
449 geometryString: 'M0 0 M0 8.85 L4.9 13.75 16.2 2.45 M16.2 16.2',
450 strokeWidth: 2,
451 stretch: go.GraphObject.Fill,
452 geometryStretch: go.GraphObject.Uniform,
453 visible: false // visible set to false: not checked, unless data.PROPNAME is true
454 },
455 // create a data Binding only if PROPNAME is supplied and not the empty string
456 (propname !== '' ? new go.Binding('visible', propname).makeTwoWay() : []))));
457 button.click = (e, btn) => {
458 const diagram = e.diagram;
459 if (diagram === null || diagram.isReadOnly)
460 return;
461 if (propname !== '' && diagram.model.isReadOnly)
462 return;
463 e.handled = true;
464 const shape = btn.findObject('ButtonIcon');
465 diagram.startTransaction('checkbox');
466 if (shape !== null)
467 shape.visible = !shape.visible; // this toggles data.checked due to TwoWay Binding
468 // support extra side-effects without clobbering the click event handler:
469 if (typeof btn['_doClick'] === 'function')
470 btn['_doClick'](e, btn);
471 diagram.commitTransaction('checkbox');
472 };
473 return button;
474});
475// This defines a whole check-box -- including both a 'CheckBoxButton' and whatever you want as the check box label.
476// Note that mouseEnter/mouseLeave/click events apply to everything in the panel, not just in the 'CheckBoxButton'.
477// Examples:
478// $('CheckBox', 'aBooleanDataProperty', $(go.TextBlock, 'the checkbox label'))
479// or
480// $('CheckBox', 'someProperty', $(go.TextBlock, 'A choice'),
481// { '_doClick': function(e, obj) { ... perform extra side-effects ... } })
482go.GraphObject.defineBuilder('CheckBox', (args) => {
483 // process the one required string argument for this kind of button
484 const propname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args));
485 const button = /** @type {Panel} */ (go.GraphObject.make('CheckBoxButton', propname, // bound to this data property
486 {
487 name: 'Button',
488 isActionable: false,
489 margin: new go.Margin(0, 1, 0, 0)
490 }));
491 const box = /** @type {Panel} */ (go.GraphObject.make(go.Panel, 'Horizontal', button, {
492 isActionable: true,
493 cursor: button.cursor,
494 margin: 1,
495 // transfer CheckBoxButton properties over to this new CheckBox panel
496 '_buttonFillNormal': button['_buttonFillNormal'],
497 '_buttonStrokeNormal': button['_buttonStrokeNormal'],
498 '_buttonFillOver': button['_buttonFillOver'],
499 '_buttonStrokeOver': button['_buttonStrokeOver'],
500 '_buttonFillPressed': button['_buttonFillPressed'],
501 '_buttonStrokePressed': button['_buttonStrokePressed'],
502 '_buttonFillDisabled': button['_buttonFillDisabled'],
503 mouseEnter: button.mouseEnter,
504 mouseLeave: button.mouseLeave,
505 actionDown: button.actionDown,
506 actionUp: button.actionUp,
507 actionCancel: button.actionCancel,
508 actionMove: button.actionMove,
509 click: button.click,
510 // also save original Button behavior, for potential use in a Panel.click event handler
511 '_buttonClick': button.click
512 }));
513 // avoid potentially conflicting event handlers on the 'CheckBoxButton'
514 button.mouseEnter = null;
515 button.mouseLeave = null;
516 button.actionDown = null;
517 button.actionUp = null;
518 button.actionCancel = null;
519 button.actionMove = null;
520 button.click = null;
521 return box;
522});