UNPKG

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