UNPKG

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