UNPKG

15.6 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/**
16 * This class implements a zoom slider for GoJS diagrams.
17 * The constructor takes two arguments:
18 * - `diagram` ***Diagram*** a reference to a GoJS Diagram
19 * - `options` ***Object*** an optional JS Object describing options for the slider
20 *
21 * Options:
22 * - `alignment` ***Spot*** see {@link #alignment}
23 * - `alignmentFocus` ***Spot*** see {@link #alignmentFocus}
24 * - `size` ***number*** see {@link #size}
25 * - `buttonSize` ***number*** see {@link #buttonSize}
26 * - `orientation` ***string*** see {@link #orientation}
27 * - `opacity` ***number*** see {@link #opacity}
28 *
29 * Example usage of ZoomSlider:
30 * ```js
31 * var zoomSlider = new ZoomSlider(myDiagram,
32 * {
33 * alignment: go.Spot.TopRight, alignmentFocus: go.Spot.TopRight,
34 * size: 150, buttonSize: 30, orientation: 'horizontal'
35 * });
36 * ```
37 *
38 * This is the basic HTML Structure that the ZoomSlider creates as a sibling div of the diagram:
39 * ```html
40 * <div class="zoomSlider">
41 * <button id="zoomSliderOut" class="zoomButton">-</button>
42 * <div id="zoomSliderRangeCtn" class="zoomRangeContainer">
43 * <input id="zoomSliderRange" class="zoomRangeInput" type="range" min="-50" max="100">
44 * </div>
45 * <button id="zoomSliderIn" class="zoomButton">+</button>
46 * </div>
47 * ```
48 *
49 * <p class="box">
50 * The diagram div's parent element should use `position: relative` to ensure the slider gets positioned properly.
51 *
52 * If you want to experiment with this extension, try the <a href="../../extensionsJSM/ZoomSlider.html">Zoom Slider</a> sample.
53 * @category Extension
54 */
55export class ZoomSlider {
56 private _diagram: go.Diagram;
57 private _initialScale: number;
58 private _diagramDiv: HTMLDivElement | null;
59 private _sliderDiv: HTMLDivElement | null;
60
61 // Slider options defaults:
62 private _size: number = 125;
63 private _buttonSize: number = 25;
64 private _alignment: go.Spot = go.Spot.BottomRight;
65 private _alignmentFocus: go.Spot = go.Spot.BottomRight;
66 private _orientation: string = 'vertical';
67 private _opacity: number = .75;
68
69 // Function used to keep the slider up to date
70 private updateOnViewportBoundsChanged: ((e: go.DiagramEvent) => void);
71
72 /**
73 * Constructs a ZoomSlider and sets up properties based on the options provided.
74 * Also sets up change listeners on the Diagram so the ZoomSlider stays up-to-date.
75 * @param {Diagram} diagram a reference to a GoJS Diagram
76 * @param {Object=} options an optional JS Object describing options for the slider
77 */
78 constructor(diagram: go.Diagram, options?: { [index: string]: any}) {
79 this._diagram = diagram;
80 this._initialScale = diagram.scale;
81 this._diagramDiv = diagram.div;
82 this._sliderDiv = null;
83
84 // Set properties based on options
85 if (options !== undefined) {
86 if (options.size !== undefined) this._size = options.size;
87 if (options.buttonSize !== undefined) this._buttonSize = options.buttonSize;
88 if (options.alignment !== undefined) this._alignment = options.alignment;
89 if (options.alignmentFocus !== undefined) this._alignmentFocus = options.alignmentFocus;
90 if (options.orientation !== undefined) this._orientation = options.orientation;
91 if (options.opacity !== undefined) this._opacity = options.opacity;
92 }
93
94 // Prepare change listeners
95 const self = this;
96 this.updateOnViewportBoundsChanged = (e: go.DiagramEvent) => { self.scaleToValue(); };
97
98 this.init();
99 }
100
101 /**
102 * This read-only property returns the diagram for which the slider is handling zoom.
103 */
104 get diagram(): go.Diagram { return this._diagram; }
105
106 /**
107 * Gets or sets the overall length, in pixels, that the slider will occupy.
108 * The default value is 125.
109 */
110 get size(): number { return this._size; }
111 set size(val: number) {
112 const old = this._size;
113 if (old !== val) {
114 this._size = val;
115 this.resize();
116 }
117 }
118
119 /**
120 * Gets or sets the height/width of the buttons at each end of the slider.
121 * The default value is 25.
122 */
123 get buttonSize(): number { return this._buttonSize; }
124 set buttonSize(val: number) {
125 const old = this._buttonSize;
126 if (old !== val) {
127 this._buttonSize = val;
128 this.resize();
129 }
130 }
131
132 /**
133 * Gets or sets the alignment Spot of this slider to determine where it should be placed relative to the diagram.
134 * The default value is Spot.BottomRight.
135 */
136 get alignment(): go.Spot { return this._alignment; }
137 set alignment(val: go.Spot) {
138 const old = this._alignment;
139 if (old !== val) {
140 this._alignment = val;
141 this.realign();
142 }
143 }
144
145 /**
146 * Gets or sets the Spot on this slider to be used as the alignment point when placing it relative to the diagram.
147 * The default value is Spot.BottomRight.
148 */
149 get alignmentFocus(): go.Spot { return this._alignmentFocus; }
150 set alignmentFocus(val: go.Spot) {
151 const old = this._alignmentFocus;
152 if (old !== val) {
153 this._alignmentFocus = val;
154 this.realign();
155 }
156 }
157
158 /**
159 * Gets or sets whether the slider is oriented vertically or horizontally.
160 * Must be either "horizontal" or "vertical" and is case-sensitive.
161 * The default value is `"vertical"`.
162 */
163 get orientation(): string { return this._orientation; }
164 set orientation(val: string) {
165 if (val !== 'horizontal' && val !== 'vertical') {
166 throw new Error('Orientation must be "horizontal" or "vertical"');
167 }
168 const old = this._orientation;
169 if (old !== val) {
170 this._orientation = val;
171 this.resize(true);
172 }
173 }
174
175 /**
176 * Gets or sets the opacity of the slider.
177 * The default value is 0.75.
178 */
179 get opacity(): number { return this._opacity; }
180 set opacity(val: number) {
181 const old = this._opacity;
182 if (old !== val) {
183 this._opacity = val;
184 if (this._sliderDiv !== null) {
185 this._sliderDiv.style.opacity = val.toString();
186 }
187 }
188 }
189
190 /**
191 * @ignore
192 * Initialize the slider.
193 */
194 private init() {
195 // Sets up the slider div and inner div's basic attributes and ids
196 this.sliderDivSetup();
197 this.resize(true);
198
199 // Set up the runtime code
200 this.sliderListenerSetup();
201 }
202
203 /**
204 * @ignore
205 * Create the necessary divs for the slider and add the slider as a sibling of the diagram.
206 */
207 private sliderDivSetup() {
208 this._sliderDiv = document.createElement('div');
209 this._sliderDiv.className = 'zoomSlider';
210
211 // Initialize buttons and range input
212 const zoomOutBtn = document.createElement('button');
213 zoomOutBtn.id = 'zoomSliderOut';
214 zoomOutBtn.className = 'zoomButton';
215 zoomOutBtn.innerHTML = '-';
216 this._sliderDiv.appendChild(zoomOutBtn);
217
218 const zoomRangeContainer = document.createElement('div');
219 zoomRangeContainer.id = 'zoomSliderRangeCtn';
220 zoomRangeContainer.className = 'zoomRangeContainer';
221 this._sliderDiv.appendChild(zoomRangeContainer);
222
223 const zoomRangeInput = document.createElement('input');
224 zoomRangeInput.id = 'zoomSliderRange';
225 zoomRangeInput.className = 'zoomRangeInput';
226 zoomRangeInput.type = 'range';
227 zoomRangeInput.min = '-50';
228 zoomRangeInput.max = '100';
229 zoomRangeContainer.appendChild(zoomRangeInput);
230
231 const zoomInBtn = document.createElement('button');
232 zoomInBtn.id = 'zoomSliderIn';
233 zoomInBtn.className = 'zoomButton';
234 zoomInBtn.innerHTML = '+';
235 this._sliderDiv.appendChild(zoomInBtn);
236
237 // Adds the slider as a sibling of the diagram
238 // IMPORTANT: the diagram div's parent element should use position: relative
239 if (this._diagramDiv !== null) {
240 const diagramParent = this._diagramDiv.parentElement;
241 if (diagramParent !== null) {
242 diagramParent.appendChild(this._sliderDiv);
243 }
244 }
245 }
246
247 /**
248 * @ignore
249 * Add listeners to the buttons and range input.
250 * Add a diagram listener.
251 */
252 private sliderListenerSetup() {
253 const zoomOutBtn = document.getElementById('zoomSliderOut');
254 const zoomInBtn = document.getElementById('zoomSliderIn');
255 const zoomRangeInput = document.getElementById('zoomSliderRange') as HTMLInputElement;
256
257 if (zoomOutBtn === null || zoomInBtn === null || zoomRangeInput === null) return;
258
259 // Set up diagram listener so the slider can be kept in sync with the diagram's scale
260 this.diagram.addDiagramListener('ViewportBoundsChanged', this.updateOnViewportBoundsChanged);
261
262 // Set up event handlers for buttons and input range slider
263 const self = this;
264 zoomOutBtn.onclick = function() {
265 zoomRangeInput.stepDown();
266 self.valueToScale();
267 };
268
269 zoomInBtn.onclick = function() {
270 zoomRangeInput.stepUp();
271 self.valueToScale();
272 };
273
274 const valChanged = function() {
275 self.valueToScale();
276 };
277 zoomRangeInput.oninput = valChanged;
278 zoomRangeInput.onchange = valChanged;
279 }
280
281 /**
282 * @ignore
283 * Resize the slider.
284 * @param {boolean=} reorient whether or not to reorient the slider/buttons
285 */
286 private resize(reorient?: boolean) {
287 let sliderWidth = 0;
288 let sliderHeight = 0;
289
290 const zoomOutBtn = document.getElementById('zoomSliderOut');
291 const zoomInBtn = document.getElementById('zoomSliderIn');
292 const zoomRangeContainer = document.getElementById('zoomSliderRangeCtn');
293 const zoomRangeInput = document.getElementById('zoomSliderRange');
294
295 if (this._sliderDiv === null || zoomOutBtn === null || zoomInBtn === null ||
296 zoomRangeContainer === null || zoomRangeInput === null) return;
297
298 if (this.orientation === 'horizontal') {
299 sliderWidth = this.size;
300 sliderHeight = this.buttonSize;
301 const rangeWidth = sliderWidth - sliderHeight * 2;
302
303 zoomOutBtn.style.width = sliderHeight + 'px';
304 zoomOutBtn.style.height = sliderHeight + 'px';
305
306 zoomRangeContainer.style.width = rangeWidth + 'px';
307 zoomRangeContainer.style.height = sliderHeight + 'px';
308
309 zoomRangeInput.style.width = rangeWidth + 'px';
310 zoomRangeInput.style.height = sliderHeight + 'px';
311 zoomRangeInput.style.transformOrigin = '';
312
313 zoomInBtn.style.width = sliderHeight + 'px';
314 zoomInBtn.style.height = sliderHeight + 'px';
315 } else {
316 sliderHeight = this.size;
317 sliderWidth = this.buttonSize;
318 const rangeHeight = sliderHeight - sliderWidth * 2;
319
320 zoomInBtn.style.width = sliderWidth + 'px';
321 zoomInBtn.style.height = sliderWidth + 'px';
322
323 zoomRangeContainer.style.width = sliderWidth + 'px';
324 zoomRangeContainer.style.height = rangeHeight + 'px';
325
326 zoomRangeInput.style.width = rangeHeight + 'px';
327 zoomRangeInput.style.height = sliderWidth + 'px';
328 zoomRangeInput.style.transformOrigin = rangeHeight / 2 + 'px ' + rangeHeight / 2 + 'px';
329
330 zoomOutBtn.style.width = sliderWidth + 'px';
331 zoomOutBtn.style.height = sliderWidth + 'px';
332 }
333 this._sliderDiv.style.width = sliderWidth + 'px';
334 this._sliderDiv.style.height = sliderHeight + 'px';
335
336 // Reorient the slider, if necessary
337 if (reorient) {
338 this.reorient();
339 }
340
341 // Realign based on new size
342 this.realign();
343 }
344
345 /**
346 * @ignore
347 * Reorient the slider, changing the transform and the order of the buttons within the div.
348 */
349 private reorient() {
350 const zoomOutBtn = document.getElementById('zoomSliderOut');
351 const zoomInBtn = document.getElementById('zoomSliderIn');
352 const zoomRangeInput = document.getElementById('zoomSliderRange');
353
354 if (this._sliderDiv === null || zoomOutBtn === null || zoomInBtn === null || zoomRangeInput === null) return;
355
356 // Need to set the transform of the range input and move the buttons to the correct sides
357 if (this.orientation === 'horizontal') {
358 zoomRangeInput.style.transform = '';
359 this._sliderDiv.insertBefore(zoomOutBtn, this._sliderDiv.firstChild);
360 this._sliderDiv.appendChild(zoomInBtn);
361 } else {
362 zoomRangeInput.style.transform = 'rotate(-90deg)';
363 this._sliderDiv.insertBefore(zoomInBtn, this._sliderDiv.firstChild);
364 this._sliderDiv.appendChild(zoomOutBtn);
365 }
366 }
367
368 /**
369 * @ignore
370 * Realigns to slider relative to the diagram.
371 */
372 private realign() {
373 if (this._diagramDiv === null || this._sliderDiv === null) return;
374
375 let sliderWidth = 0;
376 let sliderHeight = 0;
377 if (this.orientation === 'horizontal') {
378 sliderWidth = this.size;
379 sliderHeight = this.buttonSize;
380 } else {
381 sliderHeight = this.size;
382 sliderWidth = this.buttonSize;
383 }
384
385 // Finds the diagram and diagram's parent in the page
386 const diagramParent = this._diagramDiv.parentElement;
387 const diagramLoc = this._diagramDiv.getBoundingClientRect();
388 if (diagramParent !== null) {
389 const parentLoc = diagramParent.getBoundingClientRect();
390
391 const top = diagramLoc.top - parentLoc.top +
392 this.alignment.y * this._diagramDiv.clientHeight + this.alignment.offsetY -
393 this.alignmentFocus.y * sliderHeight + this.alignmentFocus.offsetY;
394
395 const left = diagramLoc.left - parentLoc.left +
396 this.alignment.x * this._diagramDiv.clientWidth + this.alignment.offsetX -
397 this.alignmentFocus.x * sliderWidth + this.alignmentFocus.offsetX;
398
399 this._sliderDiv.style.top = top + 'px';
400 this._sliderDiv.style.left = left + 'px';
401 }
402 }
403
404 /**
405 * @ignore
406 * Update the value of the slider input to match the diagram's scale.
407 */
408 private scaleToValue() {
409 const slider = document.getElementById('zoomSliderRange') as HTMLInputElement;
410 const diagram = this.diagram;
411 const A = this._initialScale;
412 const B = diagram.commandHandler.zoomFactor;
413 const y1 = diagram.scale;
414 slider.value = Math.round(Math.log(y1 / A) / Math.log(B)).toString();
415 }
416
417 /**
418 * @ignore
419 * Update the diagram's scale to match the value of the slider input.
420 */
421 private valueToScale() {
422 const slider = document.getElementById('zoomSliderRange') as HTMLInputElement;
423 const diagram = this.diagram;
424 const x = parseFloat(slider.value);
425 const A = this._initialScale;
426 const B = diagram.commandHandler.zoomFactor;
427 diagram.scale = A * Math.pow(B, x);
428 }
429
430 /**
431 * Remove the slider from the page.
432 */
433 public remove() {
434 // Remove the listener attached to diagram
435 this.diagram.removeDiagramListener('ViewportBoundsChanged', this.updateOnViewportBoundsChanged);
436
437 if (this._sliderDiv !== null) {
438 this._sliderDiv.innerHTML = '';
439 if (this._sliderDiv.parentElement) {
440 this._sliderDiv.parentElement.removeChild(this._sliderDiv);
441 }
442 this._sliderDiv = null;
443 }
444 }
445}