slider.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /**
  2. * Blockly Demos: SVG Slider
  3. *
  4. * Copyright 2012 Google Inc.
  5. * https://developers.google.com/blockly/
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. */
  19. /**
  20. * @fileoverview A slider control in SVG.
  21. * @author fraser@google.com (Neil Fraser)
  22. */
  23. 'use strict';
  24. /**
  25. * Object representing a horizontal slider widget.
  26. * @param {number} x The horizontal offset of the slider.
  27. * @param {number} y The vertical offset of the slider.
  28. * @param {number} width The total width of the slider.
  29. * @param {!Element} svgParent The SVG element to append the slider to.
  30. * @param {Function=} opt_changeFunc Optional callback function that will be
  31. * called when the slider is moved. The current value is passed.
  32. * @constructor
  33. */
  34. var Slider = function(x, y, width, svgParent, opt_changeFunc) {
  35. this.KNOB_Y_ = y - 12;
  36. this.KNOB_MIN_X_ = x + 8;
  37. this.KNOB_MAX_X_ = x + width - 8;
  38. this.TARGET_OVERHANG_ = 20;
  39. this.value_ = 0.5;
  40. this.changeFunc_ = opt_changeFunc;
  41. this.animationTasks_ = [];
  42. // Draw the slider.
  43. /*
  44. <line class="sliderTrack" x1="10" y1="35" x2="140" y2="35" />
  45. <rect style="opacity: 0" x="5" y="25" width="150" height="20" />
  46. <path id="knob"
  47. transform="translate(67, 23)"
  48. d="m 8,0 l -8,8 v 12 h 16 v -12 z" />
  49. <circle style="opacity: 0" r="20" cy="35" cx="75"></circle>
  50. */
  51. var track = document.createElementNS(Slider.SVG_NS_, 'line');
  52. track.setAttribute('class', 'sliderTrack');
  53. track.setAttribute('x1', x);
  54. track.setAttribute('y1', y);
  55. track.setAttribute('x2', x + width);
  56. track.setAttribute('y2', y);
  57. svgParent.appendChild(track);
  58. this.track_ = track;
  59. var rect = document.createElementNS(Slider.SVG_NS_, 'rect');
  60. rect.setAttribute('style', 'opacity: 0');
  61. rect.setAttribute('x', x - this.TARGET_OVERHANG_);
  62. rect.setAttribute('y', y - this.TARGET_OVERHANG_);
  63. rect.setAttribute('width', width + 2 * this.TARGET_OVERHANG_);
  64. rect.setAttribute('height', 2 * this.TARGET_OVERHANG_);
  65. rect.setAttribute('rx', this.TARGET_OVERHANG_);
  66. rect.setAttribute('ry', this.TARGET_OVERHANG_);
  67. svgParent.appendChild(rect);
  68. this.trackTarget_ = rect;
  69. var knob = document.createElementNS(Slider.SVG_NS_, 'path');
  70. knob.setAttribute('class', 'sliderKnob');
  71. knob.setAttribute('d', 'm 0,0 l -8,8 v 12 h 16 v -12 z');
  72. svgParent.appendChild(knob);
  73. this.knob_ = knob;
  74. var circle = document.createElementNS(Slider.SVG_NS_, 'circle');
  75. circle.setAttribute('style', 'opacity: 0');
  76. circle.setAttribute('r', this.TARGET_OVERHANG_);
  77. circle.setAttribute('cy', y);
  78. svgParent.appendChild(circle);
  79. this.knobTarget_ = circle;
  80. this.setValue(0.5);
  81. // Find the root SVG object.
  82. while (svgParent && svgParent.nodeName.toLowerCase() != 'svg') {
  83. svgParent = svgParent.parentNode;
  84. }
  85. this.SVG_ = svgParent;
  86. // Bind the events to this slider.
  87. Slider.bindEvent_(this.knobTarget_, 'mousedown', this, this.knobMouseDown_);
  88. Slider.bindEvent_(this.knobTarget_, 'touchstart', this, this.knobMouseDown_);
  89. Slider.bindEvent_(this.trackTarget_, 'mousedown', this, this.rectMouseDown_);
  90. Slider.bindEvent_(this.SVG_, 'mouseup', null, Slider.knobMouseUp_);
  91. Slider.bindEvent_(this.SVG_, 'touchend', null, Slider.knobMouseUp_);
  92. Slider.bindEvent_(this.SVG_, 'mousemove', null, Slider.knobMouseMove_);
  93. Slider.bindEvent_(this.SVG_, 'touchmove', null, Slider.knobMouseMove_);
  94. Slider.bindEvent_(document, 'mouseover', null, Slider.mouseOver_);
  95. };
  96. Slider.SVG_NS_ = 'http://www.w3.org/2000/svg';
  97. Slider.activeSlider_ = null;
  98. Slider.startMouseX_ = 0;
  99. Slider.startKnobX_ = 0;
  100. /**
  101. * Start a drag when clicking down on the knob.
  102. * @param {!Event} e Mouse-down event.
  103. * @private
  104. */
  105. Slider.prototype.knobMouseDown_ = function(e) {
  106. if (e.type == 'touchstart') {
  107. if (e.changedTouches.length != 1) {
  108. return;
  109. }
  110. Slider.touchToMouse_(e)
  111. }
  112. Slider.activeSlider_ = this;
  113. Slider.startMouseX_ = this.mouseToSvg_(e).x;
  114. Slider.startKnobX_ = 0;
  115. var transform = this.knob_.getAttribute('transform');
  116. if (transform) {
  117. var r = transform.match(/translate\(\s*([-\d.]+)/);
  118. if (r) {
  119. Slider.startKnobX_ = Number(r[1]);
  120. }
  121. }
  122. // Stop browser from attempting to drag the knob or
  123. // from scrolling/zooming the page.
  124. e.preventDefault();
  125. };
  126. /**
  127. * Stop a drag when clicking up anywhere.
  128. * @param {Event} e Mouse-up event.
  129. * @private
  130. */
  131. Slider.knobMouseUp_ = function(e) {
  132. Slider.activeSlider_ = null;
  133. };
  134. /**
  135. * Stop a drag when the mouse enters a node not part of the SVG.
  136. * @param {Event} e Mouse-up event.
  137. * @private
  138. */
  139. Slider.mouseOver_ = function(e) {
  140. if (!Slider.activeSlider_) {
  141. return;
  142. }
  143. var node = e.target;
  144. // Find the root SVG object.
  145. do {
  146. if (node == Slider.activeSlider_.SVG_) {
  147. return;
  148. }
  149. } while (node = node.parentNode);
  150. Slider.knobMouseUp_(e);
  151. };
  152. /**
  153. * Drag the knob to follow the mouse.
  154. * @param {!Event} e Mouse-move event.
  155. * @private
  156. */
  157. Slider.knobMouseMove_ = function(e) {
  158. var thisSlider = Slider.activeSlider_;
  159. if (!thisSlider) {
  160. return;
  161. }
  162. if (e.type == 'touchmove') {
  163. if (e.changedTouches.length != 1) {
  164. return;
  165. }
  166. Slider.touchToMouse_(e)
  167. }
  168. var x = thisSlider.mouseToSvg_(e).x - Slider.startMouseX_ +
  169. Slider.startKnobX_;
  170. thisSlider.setValue((x - thisSlider.KNOB_MIN_X_) /
  171. (thisSlider.KNOB_MAX_X_ - thisSlider.KNOB_MIN_X_));
  172. };
  173. /**
  174. * Jump to a new value when the track is clicked.
  175. * @param {!Event} e Mouse-down event.
  176. * @private
  177. */
  178. Slider.prototype.rectMouseDown_ = function(e) {
  179. if (e.type == 'touchstart') {
  180. if (e.changedTouches.length != 1) {
  181. return;
  182. }
  183. Slider.touchToMouse_(e)
  184. }
  185. var x = this.mouseToSvg_(e).x;
  186. this.animateValue((x - this.KNOB_MIN_X_) /
  187. (this.KNOB_MAX_X_ - this.KNOB_MIN_X_));
  188. };
  189. /**
  190. * Returns the slider's value (0.0 - 1.0).
  191. * @return {number} Current value.
  192. */
  193. Slider.prototype.getValue = function() {
  194. return this.value_;
  195. };
  196. /**
  197. * Animates the slider's value (0.0 - 1.0).
  198. * @param {number} value New value.
  199. */
  200. Slider.prototype.animateValue = function(value) {
  201. // Clear any ongoing animations.
  202. while (this.animationTasks_.length) {
  203. clearTimeout(this.animationTasks_.pop());
  204. }
  205. var duration = 200; // Milliseconds to animate for.
  206. var steps = 10; // Number of steps to animate.
  207. var oldValue = this.getValue();
  208. var thisSlider = this;
  209. var stepFunc = function(i) {
  210. return function() {
  211. var newVal = i * (value - oldValue) / (steps - 1) + oldValue;
  212. thisSlider.setValue(newVal);
  213. };
  214. }
  215. for (var i = 0; i < steps; i++) {
  216. this.animationTasks_.push(setTimeout(stepFunc(i), i * duration / steps));
  217. }
  218. };
  219. /**
  220. * Sets the slider's value (0.0 - 1.0).
  221. * @param {number} value New value.
  222. */
  223. Slider.prototype.setValue = function(value) {
  224. this.value_ = Math.min(Math.max(value, 0), 1);
  225. var x = this.KNOB_MIN_X_ +
  226. (this.KNOB_MAX_X_ - this.KNOB_MIN_X_) * this.value_;
  227. this.knob_.setAttribute('transform',
  228. 'translate(' + x + ',' + this.KNOB_Y_ + ')');
  229. this.knobTarget_.setAttribute('cx', x);
  230. this.changeFunc_ && this.changeFunc_(this.value_);
  231. };
  232. /**
  233. * Convert the mouse coordinates into SVG coordinates.
  234. * @param {!Object} e Object with x and y mouse coordinates.
  235. * @return {!Object} Object with x and y properties in SVG coordinates.
  236. * @private
  237. */
  238. Slider.prototype.mouseToSvg_ = function(e) {
  239. var svgPoint = this.SVG_.createSVGPoint();
  240. svgPoint.x = e.clientX;
  241. svgPoint.y = e.clientY;
  242. var matrix = this.SVG_.getScreenCTM().inverse();
  243. return svgPoint.matrixTransform(matrix);
  244. };
  245. /**
  246. * Bind an event to a function call.
  247. * @param {!Node} node Node upon which to listen.
  248. * @param {string} name Event name to listen to (e.g. 'mousedown').
  249. * @param {Object} thisObject The value of 'this' in the function.
  250. * @param {!Function} func Function to call when event is triggered.
  251. * @private
  252. */
  253. Slider.bindEvent_ = function(node, name, thisObject, func) {
  254. var wrapFunc = function(e) {
  255. func.apply(thisObject, arguments);
  256. };
  257. node.addEventListener(name, wrapFunc, false);
  258. };
  259. /**
  260. * Map the touch event's properties to be compatible with a mouse event.
  261. * @param {TouchEvent} e Event to modify.
  262. */
  263. Slider.touchToMouse_ = function(e) {
  264. var touchPoint = e.changedTouches[0];
  265. e.clientX = touchPoint.clientX;
  266. e.clientY = touchPoint.clientY;
  267. };