sliderbase.js 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Implementation of a basic slider control.
  16. *
  17. * Models a control that allows to select a sub-range within a given
  18. * range of values using two thumbs. The underlying range is modeled
  19. * as a range model, where the min thumb points to value of the
  20. * rangemodel, and the max thumb points to value + extent of the range
  21. * model.
  22. *
  23. * The currently selected range is exposed through methods
  24. * getValue() and getExtent().
  25. *
  26. * The reason for modelling the basic slider state as value + extent is
  27. * to be able to capture both, a two-thumb slider to select a range, and
  28. * a single-thumb slider to just select a value (in the latter case, extent
  29. * is always zero). We provide subclasses (twothumbslider.js and slider.js)
  30. * that model those special cases of this control.
  31. *
  32. * All rendering logic is left out, so that the subclasses can define
  33. * their own rendering. To do so, the subclasses overwrite:
  34. * - createDom
  35. * - decorateInternal
  36. * - getCssClass
  37. *
  38. * @author arv@google.com (Erik Arvidsson)
  39. */
  40. goog.provide('goog.ui.SliderBase');
  41. goog.provide('goog.ui.SliderBase.AnimationFactory');
  42. goog.provide('goog.ui.SliderBase.Orientation');
  43. goog.require('goog.Timer');
  44. goog.require('goog.a11y.aria');
  45. goog.require('goog.a11y.aria.Role');
  46. goog.require('goog.a11y.aria.State');
  47. goog.require('goog.array');
  48. goog.require('goog.asserts');
  49. goog.require('goog.dom');
  50. goog.require('goog.dom.TagName');
  51. goog.require('goog.dom.classlist');
  52. goog.require('goog.events');
  53. goog.require('goog.events.EventType');
  54. goog.require('goog.events.KeyCodes');
  55. goog.require('goog.events.KeyHandler');
  56. goog.require('goog.events.MouseWheelHandler');
  57. goog.require('goog.functions');
  58. goog.require('goog.fx.AnimationParallelQueue');
  59. goog.require('goog.fx.Dragger');
  60. goog.require('goog.fx.Transition');
  61. goog.require('goog.fx.dom.ResizeHeight');
  62. goog.require('goog.fx.dom.ResizeWidth');
  63. goog.require('goog.fx.dom.Slide');
  64. goog.require('goog.math');
  65. goog.require('goog.math.Coordinate');
  66. goog.require('goog.style');
  67. goog.require('goog.style.bidi');
  68. goog.require('goog.ui.Component');
  69. goog.require('goog.ui.RangeModel');
  70. /**
  71. * This creates a SliderBase object.
  72. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  73. * @param {(function(number):?string)=} opt_labelFn An optional function mapping
  74. * slider values to a description of the value.
  75. * @constructor
  76. * @extends {goog.ui.Component}
  77. */
  78. goog.ui.SliderBase = function(opt_domHelper, opt_labelFn) {
  79. goog.ui.Component.call(this, opt_domHelper);
  80. /**
  81. * The factory to use to generate additional animations when animating to a
  82. * new value.
  83. * @type {goog.ui.SliderBase.AnimationFactory}
  84. * @private
  85. */
  86. this.additionalAnimations_ = null;
  87. /**
  88. * The model for the range of the slider.
  89. * @protected {!goog.ui.RangeModel}
  90. */
  91. this.rangeModel = new goog.ui.RangeModel();
  92. /**
  93. * A function mapping slider values to text description.
  94. * @private {function(number):?string}
  95. */
  96. this.labelFn_ = opt_labelFn || goog.functions.NULL;
  97. // Don't use getHandler because it gets cleared in exitDocument.
  98. goog.events.listen(
  99. this.rangeModel, goog.ui.Component.EventType.CHANGE,
  100. this.handleRangeModelChange, false, this);
  101. };
  102. goog.inherits(goog.ui.SliderBase, goog.ui.Component);
  103. goog.tagUnsealableClass(goog.ui.SliderBase);
  104. /**
  105. * Event types used to listen for dragging events. Note that extent drag events
  106. * are also sent for single-thumb sliders, since the one thumb controls both
  107. * value and extent together; in this case, they can simply be ignored.
  108. * @enum {string}
  109. */
  110. goog.ui.SliderBase.EventType = {
  111. /** User started dragging the value thumb */
  112. DRAG_VALUE_START: goog.events.getUniqueId('dragvaluestart'),
  113. /** User is done dragging the value thumb */
  114. DRAG_VALUE_END: goog.events.getUniqueId('dragvalueend'),
  115. /** User started dragging the extent thumb */
  116. DRAG_EXTENT_START: goog.events.getUniqueId('dragextentstart'),
  117. /** User is done dragging the extent thumb */
  118. DRAG_EXTENT_END: goog.events.getUniqueId('dragextentend'),
  119. // Note that the following two events are sent twice, once for the value
  120. // dragger, and once of the extent dragger. If you need to differentiate
  121. // between the two, or if your code relies on receiving a single event per
  122. // START/END event, it should listen to one of the VALUE/EXTENT-specific
  123. // events.
  124. /** User started dragging a thumb */
  125. DRAG_START: goog.events.getUniqueId('dragstart'),
  126. /** User is done dragging a thumb */
  127. DRAG_END: goog.events.getUniqueId('dragend'),
  128. /** Animation on the value thumb ends */
  129. ANIMATION_END: goog.events.getUniqueId('animationend')
  130. };
  131. /**
  132. * Enum for representing the orientation of the slider.
  133. *
  134. * @enum {string}
  135. */
  136. goog.ui.SliderBase.Orientation = {
  137. VERTICAL: 'vertical',
  138. HORIZONTAL: 'horizontal'
  139. };
  140. /**
  141. * Orientation of the slider.
  142. * @type {goog.ui.SliderBase.Orientation}
  143. * @private
  144. */
  145. goog.ui.SliderBase.prototype.orientation_ =
  146. goog.ui.SliderBase.Orientation.HORIZONTAL;
  147. /** @private {goog.fx.AnimationParallelQueue} */
  148. goog.ui.SliderBase.prototype.currentAnimation_;
  149. /** @private {!goog.Timer} */
  150. goog.ui.SliderBase.prototype.incTimer_;
  151. /** @private {boolean} */
  152. goog.ui.SliderBase.prototype.incrementing_;
  153. /** @private {number} */
  154. goog.ui.SliderBase.prototype.lastMousePosition_;
  155. /**
  156. * When the user holds down the mouse on the slider background, the closest
  157. * thumb will move in "lock-step" towards the mouse. This number indicates how
  158. * long each step should take (in milliseconds).
  159. * @type {number}
  160. * @private
  161. */
  162. goog.ui.SliderBase.MOUSE_DOWN_INCREMENT_INTERVAL_ = 200;
  163. /**
  164. * How long the animations should take (in milliseconds).
  165. * @type {number}
  166. * @private
  167. */
  168. goog.ui.SliderBase.ANIMATION_INTERVAL_ = 100;
  169. /**
  170. * The minThumb dom-element, pointing to the start of the selected range.
  171. * @type {HTMLDivElement}
  172. * @protected
  173. */
  174. goog.ui.SliderBase.prototype.valueThumb;
  175. /**
  176. * The maxThumb dom-element, pointing to the end of the selected range.
  177. * @type {HTMLDivElement}
  178. * @protected
  179. */
  180. goog.ui.SliderBase.prototype.extentThumb;
  181. /**
  182. * The dom-element highlighting the selected range.
  183. * @type {HTMLDivElement}
  184. * @protected
  185. */
  186. goog.ui.SliderBase.prototype.rangeHighlight;
  187. /**
  188. * The thumb that we should be moving (only relevant when timed move is active).
  189. * @type {HTMLDivElement}
  190. * @private
  191. */
  192. goog.ui.SliderBase.prototype.thumbToMove_;
  193. /**
  194. * The object handling keyboard events.
  195. * @type {goog.events.KeyHandler}
  196. * @private
  197. */
  198. goog.ui.SliderBase.prototype.keyHandler_;
  199. /**
  200. * The object handling mouse wheel events.
  201. * @type {goog.events.MouseWheelHandler}
  202. * @private
  203. */
  204. goog.ui.SliderBase.prototype.mouseWheelHandler_;
  205. /**
  206. * The Dragger for dragging the valueThumb.
  207. * @type {goog.fx.Dragger}
  208. * @private
  209. */
  210. goog.ui.SliderBase.prototype.valueDragger_;
  211. /**
  212. * The Dragger for dragging the extentThumb.
  213. * @type {goog.fx.Dragger}
  214. * @private
  215. */
  216. goog.ui.SliderBase.prototype.extentDragger_;
  217. /**
  218. * If we are currently animating the thumb.
  219. * @private
  220. * @type {boolean}
  221. */
  222. goog.ui.SliderBase.prototype.isAnimating_ = false;
  223. /**
  224. * Whether clicking on the backgtround should move directly to that point.
  225. * @private
  226. * @type {boolean}
  227. */
  228. goog.ui.SliderBase.prototype.moveToPointEnabled_ = false;
  229. /**
  230. * The amount to increment/decrement for page up/down as well as when holding
  231. * down the mouse button on the background.
  232. * @private
  233. * @type {number}
  234. */
  235. goog.ui.SliderBase.prototype.blockIncrement_ = 10;
  236. /**
  237. * The minimal extent. The class will ensure that the extent cannot shrink
  238. * to a value smaller than minExtent.
  239. * @private
  240. * @type {number}
  241. */
  242. goog.ui.SliderBase.prototype.minExtent_ = 0;
  243. /**
  244. * Whether the slider should handle mouse wheel events.
  245. * @private
  246. * @type {boolean}
  247. */
  248. goog.ui.SliderBase.prototype.isHandleMouseWheel_ = true;
  249. /**
  250. * The time the last mousedown event was received.
  251. * @private
  252. * @type {number}
  253. */
  254. goog.ui.SliderBase.prototype.mouseDownTime_ = 0;
  255. /**
  256. * The delay after mouseDownTime_ during which a click event is ignored.
  257. * @private
  258. * @type {number}
  259. * @const
  260. */
  261. goog.ui.SliderBase.prototype.MOUSE_DOWN_DELAY_ = 1000;
  262. /**
  263. * Whether the slider is enabled or not.
  264. * @private
  265. * @type {boolean}
  266. */
  267. goog.ui.SliderBase.prototype.enabled_ = true;
  268. /**
  269. * Whether the slider implements the changes described in http://b/6324964,
  270. * making it truly RTL. This is a temporary flag to allow clients to transition
  271. * to the new behavior at their convenience. At some point it will be the
  272. * default.
  273. * @type {boolean}
  274. * @private
  275. */
  276. goog.ui.SliderBase.prototype.flipForRtl_ = false;
  277. /**
  278. * Enables/disables true RTL behavior. This should be called immediately after
  279. * construction. This is a temporary flag to allow clients to transition
  280. * to the new behavior at their convenience. At some point it will be the
  281. * default.
  282. * @param {boolean} flipForRtl True if the slider should be flipped for RTL,
  283. * false otherwise.
  284. */
  285. goog.ui.SliderBase.prototype.enableFlipForRtl = function(flipForRtl) {
  286. this.flipForRtl_ = flipForRtl;
  287. };
  288. // TODO: Make this return a base CSS class (without orientation), in subclasses.
  289. /**
  290. * Returns the CSS class applied to the slider element for the given
  291. * orientation. Subclasses must override this method.
  292. * @param {goog.ui.SliderBase.Orientation} orient The orientation.
  293. * @return {string} The CSS class applied to slider elements.
  294. * @protected
  295. */
  296. goog.ui.SliderBase.prototype.getCssClass = goog.abstractMethod;
  297. /** @override */
  298. goog.ui.SliderBase.prototype.createDom = function() {
  299. goog.ui.SliderBase.superClass_.createDom.call(this);
  300. var element = this.getDomHelper().createDom(
  301. goog.dom.TagName.DIV, this.getCssClass(this.orientation_));
  302. this.decorateInternal(element);
  303. };
  304. /**
  305. * Subclasses must implement this method and set the valueThumb and
  306. * extentThumb to non-null values. They can also set the rangeHighlight
  307. * element if a range highlight is desired.
  308. * @type {function() : void}
  309. * @protected
  310. */
  311. goog.ui.SliderBase.prototype.createThumbs = goog.abstractMethod;
  312. /**
  313. * CSS class name applied to the slider while its thumbs are being dragged.
  314. * @type {string}
  315. * @private
  316. */
  317. goog.ui.SliderBase.SLIDER_DRAGGING_CSS_CLASS_ =
  318. goog.getCssName('goog-slider-dragging');
  319. /**
  320. * CSS class name applied to a thumb while it's being dragged.
  321. * @type {string}
  322. * @private
  323. */
  324. goog.ui.SliderBase.THUMB_DRAGGING_CSS_CLASS_ =
  325. goog.getCssName('goog-slider-thumb-dragging');
  326. /**
  327. * CSS class name applied when the slider is disabled.
  328. * @type {string}
  329. * @private
  330. */
  331. goog.ui.SliderBase.DISABLED_CSS_CLASS_ =
  332. goog.getCssName('goog-slider-disabled');
  333. /** @override */
  334. goog.ui.SliderBase.prototype.decorateInternal = function(element) {
  335. goog.ui.SliderBase.superClass_.decorateInternal.call(this, element);
  336. goog.asserts.assert(element);
  337. goog.dom.classlist.add(element, this.getCssClass(this.orientation_));
  338. this.createThumbs();
  339. this.setAriaRoles();
  340. };
  341. /**
  342. * Called when the DOM for the component is for sure in the document.
  343. * Subclasses should override this method to set this element's role.
  344. * @override
  345. */
  346. goog.ui.SliderBase.prototype.enterDocument = function() {
  347. goog.ui.SliderBase.superClass_.enterDocument.call(this);
  348. // Attach the events
  349. this.valueDragger_ = new goog.fx.Dragger(this.valueThumb);
  350. this.extentDragger_ = new goog.fx.Dragger(this.extentThumb);
  351. this.valueDragger_.enableRightPositioningForRtl(this.flipForRtl_);
  352. this.extentDragger_.enableRightPositioningForRtl(this.flipForRtl_);
  353. // The slider is handling the positioning so make the defaultActions empty.
  354. this.valueDragger_.defaultAction = this.extentDragger_.defaultAction =
  355. goog.nullFunction;
  356. this.keyHandler_ = new goog.events.KeyHandler(this.getElement());
  357. this.enableEventHandlers_(true);
  358. this.getElement().tabIndex = 0;
  359. this.updateUi_();
  360. };
  361. /**
  362. * Attaches/Detaches the event handlers on the slider.
  363. * @param {boolean} enable Whether to attach or detach the event handlers.
  364. * @private
  365. */
  366. goog.ui.SliderBase.prototype.enableEventHandlers_ = function(enable) {
  367. if (enable) {
  368. this.getHandler()
  369. .listen(
  370. this.valueDragger_, goog.fx.Dragger.EventType.BEFOREDRAG,
  371. this.handleBeforeDrag_)
  372. .listen(
  373. this.extentDragger_, goog.fx.Dragger.EventType.BEFOREDRAG,
  374. this.handleBeforeDrag_)
  375. .listen(
  376. this.valueDragger_,
  377. [goog.fx.Dragger.EventType.START, goog.fx.Dragger.EventType.END],
  378. this.handleThumbDragStartEnd_)
  379. .listen(
  380. this.extentDragger_,
  381. [goog.fx.Dragger.EventType.START, goog.fx.Dragger.EventType.END],
  382. this.handleThumbDragStartEnd_)
  383. .listen(
  384. this.keyHandler_, goog.events.KeyHandler.EventType.KEY,
  385. this.handleKeyDown_)
  386. .listen(
  387. this.getElement(), goog.events.EventType.CLICK,
  388. this.handleMouseDownAndClick_)
  389. .listen(
  390. this.getElement(), goog.events.EventType.MOUSEDOWN,
  391. this.handleMouseDownAndClick_);
  392. if (this.isHandleMouseWheel()) {
  393. this.enableMouseWheelHandling_(true);
  394. }
  395. } else {
  396. this.getHandler()
  397. .unlisten(
  398. this.valueDragger_, goog.fx.Dragger.EventType.BEFOREDRAG,
  399. this.handleBeforeDrag_)
  400. .unlisten(
  401. this.extentDragger_, goog.fx.Dragger.EventType.BEFOREDRAG,
  402. this.handleBeforeDrag_)
  403. .unlisten(
  404. this.valueDragger_,
  405. [goog.fx.Dragger.EventType.START, goog.fx.Dragger.EventType.END],
  406. this.handleThumbDragStartEnd_)
  407. .unlisten(
  408. this.extentDragger_,
  409. [goog.fx.Dragger.EventType.START, goog.fx.Dragger.EventType.END],
  410. this.handleThumbDragStartEnd_)
  411. .unlisten(
  412. this.keyHandler_, goog.events.KeyHandler.EventType.KEY,
  413. this.handleKeyDown_)
  414. .unlisten(
  415. this.getElement(), goog.events.EventType.CLICK,
  416. this.handleMouseDownAndClick_)
  417. .unlisten(
  418. this.getElement(), goog.events.EventType.MOUSEDOWN,
  419. this.handleMouseDownAndClick_);
  420. if (this.isHandleMouseWheel()) {
  421. this.enableMouseWheelHandling_(false);
  422. }
  423. }
  424. };
  425. /** @override */
  426. goog.ui.SliderBase.prototype.exitDocument = function() {
  427. goog.ui.SliderBase.base(this, 'exitDocument');
  428. goog.disposeAll(
  429. this.valueDragger_, this.extentDragger_, this.keyHandler_,
  430. this.mouseWheelHandler_);
  431. };
  432. /**
  433. * Handler for the before drag event. We use the event properties to determine
  434. * the new value.
  435. * @param {goog.fx.DragEvent} e The drag event used to drag the thumb.
  436. * @private
  437. */
  438. goog.ui.SliderBase.prototype.handleBeforeDrag_ = function(e) {
  439. var thumbToDrag =
  440. e.dragger == this.valueDragger_ ? this.valueThumb : this.extentThumb;
  441. var value;
  442. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  443. var availHeight = this.getElement().clientHeight - thumbToDrag.offsetHeight;
  444. value = (availHeight - e.top) / availHeight *
  445. (this.getMaximum() - this.getMinimum()) +
  446. this.getMinimum();
  447. } else {
  448. var availWidth = this.getElement().clientWidth - thumbToDrag.offsetWidth;
  449. value = (e.left / availWidth) * (this.getMaximum() - this.getMinimum()) +
  450. this.getMinimum();
  451. }
  452. // Bind the value within valid range before calling setThumbPosition_.
  453. // This is necessary because setThumbPosition_ is a no-op for values outside
  454. // of the legal range. For drag operations, we want the handle to snap to the
  455. // last valid value instead of remaining at the previous position.
  456. if (e.dragger == this.valueDragger_) {
  457. value = Math.min(
  458. Math.max(value, this.getMinimum()), this.getValue() + this.getExtent());
  459. } else {
  460. value = Math.min(Math.max(value, this.getValue()), this.getMaximum());
  461. }
  462. this.setThumbPosition_(thumbToDrag, value);
  463. };
  464. /**
  465. * Handler for the start/end drag event on the thumbs. Adds/removes
  466. * the "-dragging" CSS classes on the slider and thumb.
  467. * @param {goog.fx.DragEvent} e The drag event used to drag the thumb.
  468. * @private
  469. */
  470. goog.ui.SliderBase.prototype.handleThumbDragStartEnd_ = function(e) {
  471. var isDragStart = e.type == goog.fx.Dragger.EventType.START;
  472. goog.dom.classlist.enable(
  473. goog.asserts.assertElement(this.getElement()),
  474. goog.ui.SliderBase.SLIDER_DRAGGING_CSS_CLASS_, isDragStart);
  475. goog.dom.classlist.enable(
  476. goog.asserts.assertElement(e.target.handle),
  477. goog.ui.SliderBase.THUMB_DRAGGING_CSS_CLASS_, isDragStart);
  478. var isValueDragger = e.dragger == this.valueDragger_;
  479. if (isDragStart) {
  480. this.dispatchEvent(goog.ui.SliderBase.EventType.DRAG_START);
  481. this.dispatchEvent(
  482. isValueDragger ? goog.ui.SliderBase.EventType.DRAG_VALUE_START :
  483. goog.ui.SliderBase.EventType.DRAG_EXTENT_START);
  484. } else {
  485. this.dispatchEvent(goog.ui.SliderBase.EventType.DRAG_END);
  486. this.dispatchEvent(
  487. isValueDragger ? goog.ui.SliderBase.EventType.DRAG_VALUE_END :
  488. goog.ui.SliderBase.EventType.DRAG_EXTENT_END);
  489. }
  490. };
  491. /**
  492. * Event handler for the key down event. This is used to update the value
  493. * based on the key pressed.
  494. * @param {goog.events.KeyEvent} e The keyboard event object.
  495. * @private
  496. */
  497. goog.ui.SliderBase.prototype.handleKeyDown_ = function(e) {
  498. var handled = true;
  499. switch (e.keyCode) {
  500. case goog.events.KeyCodes.HOME:
  501. this.animatedSetValue(this.getMinimum());
  502. break;
  503. case goog.events.KeyCodes.END:
  504. this.animatedSetValue(this.getMaximum());
  505. break;
  506. case goog.events.KeyCodes.PAGE_UP:
  507. this.moveThumbs(this.getBlockIncrement());
  508. break;
  509. case goog.events.KeyCodes.PAGE_DOWN:
  510. this.moveThumbs(-this.getBlockIncrement());
  511. break;
  512. case goog.events.KeyCodes.LEFT:
  513. var sign = this.flipForRtl_ && this.isRightToLeft() ? 1 : -1;
  514. this.moveThumbs(
  515. e.shiftKey ? sign * this.getBlockIncrement() :
  516. sign * this.getUnitIncrement());
  517. break;
  518. case goog.events.KeyCodes.DOWN:
  519. this.moveThumbs(
  520. e.shiftKey ? -this.getBlockIncrement() : -this.getUnitIncrement());
  521. break;
  522. case goog.events.KeyCodes.RIGHT:
  523. var sign = this.flipForRtl_ && this.isRightToLeft() ? -1 : 1;
  524. this.moveThumbs(
  525. e.shiftKey ? sign * this.getBlockIncrement() :
  526. sign * this.getUnitIncrement());
  527. break;
  528. case goog.events.KeyCodes.UP:
  529. this.moveThumbs(
  530. e.shiftKey ? this.getBlockIncrement() : this.getUnitIncrement());
  531. break;
  532. default:
  533. handled = false;
  534. }
  535. if (handled) {
  536. e.preventDefault();
  537. }
  538. };
  539. /**
  540. * Handler for the mouse down event and click event.
  541. * @param {goog.events.Event} e The mouse event object.
  542. * @private
  543. */
  544. goog.ui.SliderBase.prototype.handleMouseDownAndClick_ = function(e) {
  545. if (this.getElement().focus) {
  546. this.getElement().focus();
  547. }
  548. // Known Element.
  549. var target = /** @type {Element} */ (e.target);
  550. if (!goog.dom.contains(this.valueThumb, target) &&
  551. !goog.dom.contains(this.extentThumb, target)) {
  552. var isClick = e.type == goog.events.EventType.CLICK;
  553. if (isClick && goog.now() < this.mouseDownTime_ + this.MOUSE_DOWN_DELAY_) {
  554. // Ignore a click event that comes a short moment after a mousedown
  555. // event. This happens for desktop. For devices with both a touch
  556. // screen and a mouse pad we do not get a mousedown event from the mouse
  557. // pad and do get a click event.
  558. return;
  559. }
  560. if (!isClick) {
  561. this.mouseDownTime_ = goog.now();
  562. }
  563. if (this.moveToPointEnabled_) {
  564. // just set the value directly based on the position of the click
  565. this.animatedSetValue(this.getValueFromMousePosition(e));
  566. } else {
  567. // start a timer that incrementally moves the handle
  568. this.startBlockIncrementing_(e);
  569. }
  570. }
  571. };
  572. /**
  573. * Handler for the mouse wheel event.
  574. * @param {goog.events.MouseWheelEvent} e The mouse wheel event object.
  575. * @private
  576. */
  577. goog.ui.SliderBase.prototype.handleMouseWheel_ = function(e) {
  578. // Just move one unit increment per mouse wheel event
  579. var direction = e.detail > 0 ? -1 : 1;
  580. this.moveThumbs(direction * this.getUnitIncrement());
  581. e.preventDefault();
  582. };
  583. /**
  584. * Starts the animation that causes the thumb to increment/decrement by the
  585. * block increment when the user presses down on the background.
  586. * @param {goog.events.Event} e The mouse event object.
  587. * @private
  588. */
  589. goog.ui.SliderBase.prototype.startBlockIncrementing_ = function(e) {
  590. this.storeMousePos_(e);
  591. this.thumbToMove_ = this.getClosestThumb_(this.getValueFromMousePosition(e));
  592. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  593. this.incrementing_ = this.lastMousePosition_ < this.thumbToMove_.offsetTop;
  594. } else {
  595. this.incrementing_ = this.lastMousePosition_ >
  596. this.getOffsetStart_(this.thumbToMove_) + this.thumbToMove_.offsetWidth;
  597. }
  598. var doc = goog.dom.getOwnerDocument(this.getElement());
  599. this.getHandler()
  600. .listen(
  601. doc, goog.events.EventType.MOUSEUP, this.stopBlockIncrementing_, true)
  602. .listen(
  603. this.getElement(), goog.events.EventType.MOUSEMOVE,
  604. this.storeMousePos_);
  605. if (!this.incTimer_) {
  606. this.incTimer_ =
  607. new goog.Timer(goog.ui.SliderBase.MOUSE_DOWN_INCREMENT_INTERVAL_);
  608. this.getHandler().listen(
  609. this.incTimer_, goog.Timer.TICK, this.handleTimerTick_);
  610. }
  611. this.handleTimerTick_();
  612. this.incTimer_.start();
  613. };
  614. /**
  615. * Handler for the tick event dispatched by the timer used to update the value
  616. * in a block increment. This is also called directly from
  617. * startBlockIncrementing_.
  618. * @private
  619. */
  620. goog.ui.SliderBase.prototype.handleTimerTick_ = function() {
  621. var value;
  622. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  623. var mouseY = this.lastMousePosition_;
  624. var thumbY = this.thumbToMove_.offsetTop;
  625. if (this.incrementing_) {
  626. if (mouseY < thumbY) {
  627. value = this.getThumbPosition_(this.thumbToMove_) +
  628. this.getBlockIncrement();
  629. }
  630. } else {
  631. var thumbH = this.thumbToMove_.offsetHeight;
  632. if (mouseY > thumbY + thumbH) {
  633. value = this.getThumbPosition_(this.thumbToMove_) -
  634. this.getBlockIncrement();
  635. }
  636. }
  637. } else {
  638. var mouseX = this.lastMousePosition_;
  639. var thumbX = this.getOffsetStart_(this.thumbToMove_);
  640. if (this.incrementing_) {
  641. var thumbW = this.thumbToMove_.offsetWidth;
  642. if (mouseX > thumbX + thumbW) {
  643. value = this.getThumbPosition_(this.thumbToMove_) +
  644. this.getBlockIncrement();
  645. }
  646. } else {
  647. if (mouseX < thumbX) {
  648. value = this.getThumbPosition_(this.thumbToMove_) -
  649. this.getBlockIncrement();
  650. }
  651. }
  652. }
  653. if (goog.isDef(value)) { // not all code paths sets the value variable
  654. this.setThumbPosition_(this.thumbToMove_, value);
  655. }
  656. };
  657. /**
  658. * Stops the block incrementing animation and unlistens the necessary
  659. * event handlers.
  660. * @private
  661. */
  662. goog.ui.SliderBase.prototype.stopBlockIncrementing_ = function() {
  663. if (this.incTimer_) {
  664. this.incTimer_.stop();
  665. }
  666. var doc = goog.dom.getOwnerDocument(this.getElement());
  667. this.getHandler()
  668. .unlisten(
  669. doc, goog.events.EventType.MOUSEUP, this.stopBlockIncrementing_, true)
  670. .unlisten(
  671. this.getElement(), goog.events.EventType.MOUSEMOVE,
  672. this.storeMousePos_);
  673. };
  674. /**
  675. * Returns the relative mouse position to the slider.
  676. * @param {goog.events.Event} e The mouse event object.
  677. * @return {number} The relative mouse position to the slider.
  678. * @private
  679. */
  680. goog.ui.SliderBase.prototype.getRelativeMousePos_ = function(e) {
  681. var coord = goog.style.getRelativePosition(e, this.getElement());
  682. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  683. return coord.y;
  684. } else {
  685. if (this.flipForRtl_ && this.isRightToLeft()) {
  686. return this.getElement().clientWidth - coord.x;
  687. } else {
  688. return coord.x;
  689. }
  690. }
  691. };
  692. /**
  693. * Stores the current mouse position so that it can be used in the timer.
  694. * @param {goog.events.Event} e The mouse event object.
  695. * @private
  696. */
  697. goog.ui.SliderBase.prototype.storeMousePos_ = function(e) {
  698. this.lastMousePosition_ = this.getRelativeMousePos_(e);
  699. };
  700. /**
  701. * Returns the value to use for the current mouse position
  702. * @param {goog.events.Event} e The mouse event object.
  703. * @return {number} The value that this mouse position represents.
  704. */
  705. goog.ui.SliderBase.prototype.getValueFromMousePosition = function(e) {
  706. var min = this.getMinimum();
  707. var max = this.getMaximum();
  708. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  709. var thumbH = this.valueThumb.offsetHeight;
  710. var availH = this.getElement().clientHeight - thumbH;
  711. var y = this.getRelativeMousePos_(e) - thumbH / 2;
  712. return (max - min) * (availH - y) / availH + min;
  713. } else {
  714. var thumbW = this.valueThumb.offsetWidth;
  715. var availW = this.getElement().clientWidth - thumbW;
  716. var x = this.getRelativeMousePos_(e) - thumbW / 2;
  717. return (max - min) * x / availW + min;
  718. }
  719. };
  720. /**
  721. * @param {HTMLDivElement} thumb The thumb object.
  722. * @return {number} The position of the specified thumb.
  723. * @private
  724. */
  725. goog.ui.SliderBase.prototype.getThumbPosition_ = function(thumb) {
  726. if (thumb == this.valueThumb) {
  727. return this.rangeModel.getValue();
  728. } else if (thumb == this.extentThumb) {
  729. return this.rangeModel.getValue() + this.rangeModel.getExtent();
  730. } else {
  731. throw Error('Illegal thumb element. Neither minThumb nor maxThumb');
  732. }
  733. };
  734. /**
  735. * Returns whether a thumb is currently being dragged with the mouse (or via
  736. * touch). Note that changing the value with keyboard, mouswheel, or via
  737. * move-to-point click immediately sends a CHANGE event without going through a
  738. * dragged state.
  739. * @return {boolean} Whether a dragger is currently being dragged.
  740. */
  741. goog.ui.SliderBase.prototype.isDragging = function() {
  742. return this.valueDragger_.isDragging() || this.extentDragger_.isDragging();
  743. };
  744. /**
  745. * Moves the thumbs by the specified delta as follows
  746. * - as long as both thumbs stay within [min,max], both thumbs are moved
  747. * - once a thumb reaches or exceeds min (or max, respectively), it stays
  748. * - at min (or max, respectively).
  749. * In case both thumbs have reached min (or max), no change event will fire.
  750. * If the specified delta is smaller than the step size, it will be rounded
  751. * to the step size.
  752. * @param {number} delta The delta by which to move the selected range.
  753. */
  754. goog.ui.SliderBase.prototype.moveThumbs = function(delta) {
  755. // Assume that a small delta is supposed to be at least a step.
  756. if (Math.abs(delta) < this.getStep()) {
  757. delta = goog.math.sign(delta) * this.getStep();
  758. }
  759. var newMinPos = this.getThumbPosition_(this.valueThumb) + delta;
  760. var newMaxPos = this.getThumbPosition_(this.extentThumb) + delta;
  761. // correct min / max positions to be within bounds
  762. newMinPos = goog.math.clamp(
  763. newMinPos, this.getMinimum(), this.getMaximum() - this.minExtent_);
  764. newMaxPos = goog.math.clamp(
  765. newMaxPos, this.getMinimum() + this.minExtent_, this.getMaximum());
  766. // Set value and extent atomically
  767. this.setValueAndExtent(newMinPos, newMaxPos - newMinPos);
  768. };
  769. /**
  770. * Sets the position of the given thumb. The set is ignored and no CHANGE event
  771. * fires if it violates the constraint minimum <= value (valueThumb position) <=
  772. * value + extent (extentThumb position) <= maximum.
  773. *
  774. * Note: To keep things simple, the setThumbPosition_ function does not have the
  775. * side-effect of "correcting" value or extent to fit the above constraint as it
  776. * is the case in the underlying range model. Instead, we simply ignore the
  777. * call. Callers must make these adjustements explicitly if they wish.
  778. * @param {Element} thumb The thumb whose position to set.
  779. * @param {number} position The position to move the thumb to.
  780. * @private
  781. */
  782. goog.ui.SliderBase.prototype.setThumbPosition_ = function(thumb, position) {
  783. // Round first so that all computations and checks are consistent.
  784. var roundedPosition = this.rangeModel.roundToStepWithMin(position);
  785. var value =
  786. thumb == this.valueThumb ? roundedPosition : this.rangeModel.getValue();
  787. var end = thumb == this.extentThumb ?
  788. roundedPosition :
  789. this.rangeModel.getValue() + this.rangeModel.getExtent();
  790. if (value >= this.getMinimum() && end >= value + this.minExtent_ &&
  791. this.getMaximum() >= end) {
  792. this.setValueAndExtent(value, end - value);
  793. }
  794. };
  795. /**
  796. * Sets the value and extent of the underlying range model. We enforce that
  797. * getMinimum() <= value <= getMaximum() - extent and
  798. * getMinExtent <= extent <= getMaximum() - getValue()
  799. * If this is not satisfied for the given extent, the call is ignored and no
  800. * CHANGE event fires. This is a utility method to allow setting the thumbs
  801. * simultaneously and ensuring that only one event fires.
  802. * @param {number} value The value to which to set the value.
  803. * @param {number} extent The value to which to set the extent.
  804. */
  805. goog.ui.SliderBase.prototype.setValueAndExtent = function(value, extent) {
  806. if (this.getMinimum() <= value && value <= this.getMaximum() - extent &&
  807. this.minExtent_ <= extent && extent <= this.getMaximum() - value) {
  808. if (value == this.getValue() && extent == this.getExtent()) {
  809. return;
  810. }
  811. // because the underlying range model applies adjustements of value
  812. // and extent to fit within bounds, we need to reset the extent
  813. // first so these adjustements don't kick in.
  814. this.rangeModel.setMute(true);
  815. this.rangeModel.setExtent(0);
  816. this.rangeModel.setValue(value);
  817. this.rangeModel.setExtent(extent);
  818. this.rangeModel.setMute(false);
  819. this.handleRangeModelChange(null);
  820. }
  821. };
  822. /**
  823. * @return {number} The minimum value.
  824. */
  825. goog.ui.SliderBase.prototype.getMinimum = function() {
  826. return this.rangeModel.getMinimum();
  827. };
  828. /**
  829. * Sets the minimum number.
  830. * @param {number} min The minimum value.
  831. */
  832. goog.ui.SliderBase.prototype.setMinimum = function(min) {
  833. this.rangeModel.setMinimum(min);
  834. };
  835. /**
  836. * @return {number} The maximum value.
  837. */
  838. goog.ui.SliderBase.prototype.getMaximum = function() {
  839. return this.rangeModel.getMaximum();
  840. };
  841. /**
  842. * Sets the maximum number.
  843. * @param {number} max The maximum value.
  844. */
  845. goog.ui.SliderBase.prototype.setMaximum = function(max) {
  846. this.rangeModel.setMaximum(max);
  847. };
  848. /**
  849. * @return {HTMLDivElement} The value thumb element.
  850. */
  851. goog.ui.SliderBase.prototype.getValueThumb = function() {
  852. return this.valueThumb;
  853. };
  854. /**
  855. * @return {HTMLDivElement} The extent thumb element.
  856. */
  857. goog.ui.SliderBase.prototype.getExtentThumb = function() {
  858. return this.extentThumb;
  859. };
  860. /**
  861. * @param {number} position The position to get the closest thumb to.
  862. * @return {HTMLDivElement} The thumb that is closest to the given position.
  863. * @private
  864. */
  865. goog.ui.SliderBase.prototype.getClosestThumb_ = function(position) {
  866. if (position <=
  867. (this.rangeModel.getValue() + this.rangeModel.getExtent() / 2)) {
  868. return this.valueThumb;
  869. } else {
  870. return this.extentThumb;
  871. }
  872. };
  873. /**
  874. * Call back when the internal range model changes. Sub-classes may override
  875. * and re-enter this method to update a11y state. Consider protected.
  876. * @param {goog.events.Event} e The event object.
  877. * @protected
  878. */
  879. goog.ui.SliderBase.prototype.handleRangeModelChange = function(e) {
  880. this.updateUi_();
  881. this.updateAriaStates();
  882. this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
  883. };
  884. /**
  885. * This is called when we need to update the size of the thumb. This happens
  886. * when first created as well as when the value and the orientation changes.
  887. * @private
  888. */
  889. goog.ui.SliderBase.prototype.updateUi_ = function() {
  890. if (this.valueThumb && !this.isAnimating_) {
  891. var minCoord = this.getThumbCoordinateForValue(
  892. this.getThumbPosition_(this.valueThumb));
  893. var maxCoord = this.getThumbCoordinateForValue(
  894. this.getThumbPosition_(this.extentThumb));
  895. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  896. this.valueThumb.style.top = minCoord.y + 'px';
  897. this.extentThumb.style.top = maxCoord.y + 'px';
  898. if (this.rangeHighlight) {
  899. var highlightPositioning = this.calculateRangeHighlightPositioning_(
  900. maxCoord.y, minCoord.y, this.valueThumb.offsetHeight);
  901. this.rangeHighlight.style.top = highlightPositioning.offset + 'px';
  902. this.rangeHighlight.style.height = highlightPositioning.size + 'px';
  903. }
  904. } else {
  905. var pos = (this.flipForRtl_ && this.isRightToLeft()) ? 'right' : 'left';
  906. this.valueThumb.style[pos] = minCoord.x + 'px';
  907. this.extentThumb.style[pos] = maxCoord.x + 'px';
  908. if (this.rangeHighlight) {
  909. var highlightPositioning = this.calculateRangeHighlightPositioning_(
  910. minCoord.x, maxCoord.x, this.valueThumb.offsetWidth);
  911. this.rangeHighlight.style[pos] = highlightPositioning.offset + 'px';
  912. this.rangeHighlight.style.width = highlightPositioning.size + 'px';
  913. }
  914. }
  915. }
  916. };
  917. /**
  918. * Calculates the start position (offset) and size of the range highlight, e.g.
  919. * for a horizontal slider, this will return [left, width] for the highlight.
  920. * @param {number} firstThumbPos The position of the first thumb along the
  921. * slider axis.
  922. * @param {number} secondThumbPos The position of the second thumb along the
  923. * slider axis, must be >= firstThumbPos.
  924. * @param {number} thumbSize The size of the thumb, along the slider axis.
  925. * @return {{offset: number, size: number}} The positioning parameters for the
  926. * range highlight.
  927. * @private
  928. */
  929. goog.ui.SliderBase.prototype.calculateRangeHighlightPositioning_ = function(
  930. firstThumbPos, secondThumbPos, thumbSize) {
  931. // Highlight is inset by half the thumb size, from the edges of the thumb.
  932. var highlightInset = Math.ceil(thumbSize / 2);
  933. var size = secondThumbPos - firstThumbPos + thumbSize - 2 * highlightInset;
  934. // Don't return negative size since it causes an error. IE sometimes attempts
  935. // to position the thumbs while slider size is 0, resulting in size < 0 here.
  936. return {offset: firstThumbPos + highlightInset, size: Math.max(size, 0)};
  937. };
  938. /**
  939. * Returns the position to move the handle to for a given value
  940. * @param {number} val The value to get the coordinate for.
  941. * @return {!goog.math.Coordinate} Coordinate with either x or y set.
  942. */
  943. goog.ui.SliderBase.prototype.getThumbCoordinateForValue = function(val) {
  944. var coord = new goog.math.Coordinate;
  945. if (this.valueThumb) {
  946. var min = this.getMinimum();
  947. var max = this.getMaximum();
  948. // This check ensures the ratio never take NaN value, which is possible when
  949. // the slider min & max are same numbers (i.e. 1).
  950. var ratio = (val == min && min == max) ? 0 : (val - min) / (max - min);
  951. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  952. var thumbHeight = this.valueThumb.offsetHeight;
  953. var h = this.getElement().clientHeight - thumbHeight;
  954. var bottom = Math.round(ratio * h);
  955. coord.x = this.getOffsetStart_(this.valueThumb); // Keep x the same.
  956. coord.y = h - bottom;
  957. } else {
  958. var w = this.getElement().clientWidth - this.valueThumb.offsetWidth;
  959. var left = Math.round(ratio * w);
  960. coord.x = left;
  961. coord.y = this.valueThumb.offsetTop; // Keep y the same.
  962. }
  963. }
  964. return coord;
  965. };
  966. /**
  967. * Sets the value and starts animating the handle towards that position.
  968. * @param {number} v Value to set and animate to.
  969. */
  970. goog.ui.SliderBase.prototype.animatedSetValue = function(v) {
  971. // the value might be out of bounds
  972. v = goog.math.clamp(v, this.getMinimum(), this.getMaximum());
  973. if (this.isAnimating_) {
  974. this.currentAnimation_.stop(true);
  975. this.currentAnimation_.dispose();
  976. }
  977. var animations = new goog.fx.AnimationParallelQueue();
  978. var end;
  979. var thumb = this.getClosestThumb_(v);
  980. var previousValue = this.getValue();
  981. var previousExtent = this.getExtent();
  982. var previousThumbValue = this.getThumbPosition_(thumb);
  983. var previousCoord = this.getThumbCoordinateForValue(previousThumbValue);
  984. var stepSize = this.getStep();
  985. // If the delta is less than a single step, increase it to a step, else the
  986. // range model will reduce it to zero.
  987. if (Math.abs(v - previousThumbValue) < stepSize) {
  988. var delta = v > previousThumbValue ? stepSize : -stepSize;
  989. v = previousThumbValue + delta;
  990. // The resulting value may be out of bounds, sanitize.
  991. v = goog.math.clamp(v, this.getMinimum(), this.getMaximum());
  992. }
  993. this.setThumbPosition_(thumb, v);
  994. var coord = this.getThumbCoordinateForValue(this.getThumbPosition_(thumb));
  995. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  996. end = [this.getOffsetStart_(thumb), coord.y];
  997. } else {
  998. end = [coord.x, thumb.offsetTop];
  999. }
  1000. var slide = new goog.fx.dom.Slide(
  1001. thumb, [previousCoord.x, previousCoord.y], end,
  1002. goog.ui.SliderBase.ANIMATION_INTERVAL_);
  1003. slide.enableRightPositioningForRtl(this.flipForRtl_);
  1004. animations.add(slide);
  1005. if (this.rangeHighlight) {
  1006. this.addRangeHighlightAnimations_(
  1007. thumb, previousValue, previousExtent, coord, animations);
  1008. }
  1009. // Create additional animations to play if a factory has been set.
  1010. if (this.additionalAnimations_) {
  1011. var additionalAnimations = this.additionalAnimations_.createAnimations(
  1012. previousValue, v, goog.ui.SliderBase.ANIMATION_INTERVAL_);
  1013. goog.array.forEach(additionalAnimations, function(animation) {
  1014. animations.add(animation);
  1015. });
  1016. }
  1017. this.currentAnimation_ = animations;
  1018. this.getHandler().listen(
  1019. animations, goog.fx.Transition.EventType.END, this.endAnimation_);
  1020. this.isAnimating_ = true;
  1021. animations.play(false);
  1022. };
  1023. /**
  1024. * @return {boolean} True if the slider is animating, false otherwise.
  1025. */
  1026. goog.ui.SliderBase.prototype.isAnimating = function() {
  1027. return this.isAnimating_;
  1028. };
  1029. /**
  1030. * Sets the factory that will be used to create additional animations to be
  1031. * played when animating to a new value. These animations can be for any
  1032. * element and the animations will be played in addition to the default
  1033. * animation(s). The animations will also be played in the same parallel queue
  1034. * ensuring that all animations are played at the same time.
  1035. * @see #animatedSetValue
  1036. *
  1037. * @param {goog.ui.SliderBase.AnimationFactory} factory The animation factory to
  1038. * use. This will not change the default animations played by the slider.
  1039. * It will only allow for additional animations.
  1040. */
  1041. goog.ui.SliderBase.prototype.setAdditionalAnimations = function(factory) {
  1042. this.additionalAnimations_ = factory;
  1043. };
  1044. /**
  1045. * Adds animations for the range highlight element to the animation queue.
  1046. *
  1047. * @param {Element} thumb The thumb that's moving, must be
  1048. * either valueThumb or extentThumb.
  1049. * @param {number} previousValue The previous value of the slider.
  1050. * @param {number} previousExtent The previous extent of the
  1051. * slider.
  1052. * @param {goog.math.Coordinate} newCoord The new pixel coordinate of the
  1053. * thumb that's moving.
  1054. * @param {goog.fx.AnimationParallelQueue} animations The animation queue.
  1055. * @private
  1056. */
  1057. goog.ui.SliderBase.prototype.addRangeHighlightAnimations_ = function(
  1058. thumb, previousValue, previousExtent, newCoord, animations) {
  1059. var previousMinCoord = this.getThumbCoordinateForValue(previousValue);
  1060. var previousMaxCoord =
  1061. this.getThumbCoordinateForValue(previousValue + previousExtent);
  1062. var minCoord = previousMinCoord;
  1063. var maxCoord = previousMaxCoord;
  1064. if (thumb == this.valueThumb) {
  1065. minCoord = newCoord;
  1066. } else {
  1067. maxCoord = newCoord;
  1068. }
  1069. if (this.orientation_ == goog.ui.SliderBase.Orientation.VERTICAL) {
  1070. var previousHighlightPositioning = this.calculateRangeHighlightPositioning_(
  1071. previousMaxCoord.y, previousMinCoord.y, this.valueThumb.offsetHeight);
  1072. var highlightPositioning = this.calculateRangeHighlightPositioning_(
  1073. maxCoord.y, minCoord.y, this.valueThumb.offsetHeight);
  1074. var slide = new goog.fx.dom.Slide(
  1075. this.rangeHighlight,
  1076. [
  1077. this.getOffsetStart_(this.rangeHighlight),
  1078. previousHighlightPositioning.offset
  1079. ],
  1080. [
  1081. this.getOffsetStart_(this.rangeHighlight), highlightPositioning.offset
  1082. ],
  1083. goog.ui.SliderBase.ANIMATION_INTERVAL_);
  1084. var resizeHeight = new goog.fx.dom.ResizeHeight(
  1085. this.rangeHighlight, previousHighlightPositioning.size,
  1086. highlightPositioning.size, goog.ui.SliderBase.ANIMATION_INTERVAL_);
  1087. slide.enableRightPositioningForRtl(this.flipForRtl_);
  1088. resizeHeight.enableRightPositioningForRtl(this.flipForRtl_);
  1089. animations.add(slide);
  1090. animations.add(resizeHeight);
  1091. } else {
  1092. var previousHighlightPositioning = this.calculateRangeHighlightPositioning_(
  1093. previousMinCoord.x, previousMaxCoord.x, this.valueThumb.offsetWidth);
  1094. var highlightPositioning = this.calculateRangeHighlightPositioning_(
  1095. minCoord.x, maxCoord.x, this.valueThumb.offsetWidth);
  1096. var slide = new goog.fx.dom.Slide(
  1097. this.rangeHighlight,
  1098. [previousHighlightPositioning.offset, this.rangeHighlight.offsetTop],
  1099. [highlightPositioning.offset, this.rangeHighlight.offsetTop],
  1100. goog.ui.SliderBase.ANIMATION_INTERVAL_);
  1101. var resizeWidth = new goog.fx.dom.ResizeWidth(
  1102. this.rangeHighlight, previousHighlightPositioning.size,
  1103. highlightPositioning.size, goog.ui.SliderBase.ANIMATION_INTERVAL_);
  1104. slide.enableRightPositioningForRtl(this.flipForRtl_);
  1105. resizeWidth.enableRightPositioningForRtl(this.flipForRtl_);
  1106. animations.add(slide);
  1107. animations.add(resizeWidth);
  1108. }
  1109. };
  1110. /**
  1111. * Sets the isAnimating_ field to false once the animation is done.
  1112. * @param {goog.fx.AnimationEvent} e Event object passed by the animation
  1113. * object.
  1114. * @private
  1115. */
  1116. goog.ui.SliderBase.prototype.endAnimation_ = function(e) {
  1117. this.isAnimating_ = false;
  1118. this.dispatchEvent(goog.ui.SliderBase.EventType.ANIMATION_END);
  1119. };
  1120. /**
  1121. * Changes the orientation.
  1122. * @param {goog.ui.SliderBase.Orientation} orient The orientation.
  1123. */
  1124. goog.ui.SliderBase.prototype.setOrientation = function(orient) {
  1125. if (this.orientation_ != orient) {
  1126. var oldCss = this.getCssClass(this.orientation_);
  1127. var newCss = this.getCssClass(orient);
  1128. this.orientation_ = orient;
  1129. // Update the DOM
  1130. if (this.getElement()) {
  1131. goog.dom.classlist.swap(
  1132. goog.asserts.assert(this.getElement()), oldCss, newCss);
  1133. // we need to reset the left and top, plus range highlight
  1134. var pos = (this.flipForRtl_ && this.isRightToLeft()) ? 'right' : 'left';
  1135. this.valueThumb.style[pos] = this.valueThumb.style.top = '';
  1136. this.extentThumb.style[pos] = this.extentThumb.style.top = '';
  1137. if (this.rangeHighlight) {
  1138. this.rangeHighlight.style[pos] = this.rangeHighlight.style.top = '';
  1139. this.rangeHighlight.style.width = this.rangeHighlight.style.height = '';
  1140. }
  1141. this.updateUi_();
  1142. }
  1143. }
  1144. };
  1145. /**
  1146. * @return {goog.ui.SliderBase.Orientation} the orientation of the slider.
  1147. */
  1148. goog.ui.SliderBase.prototype.getOrientation = function() {
  1149. return this.orientation_;
  1150. };
  1151. /** @override */
  1152. goog.ui.SliderBase.prototype.disposeInternal = function() {
  1153. goog.ui.SliderBase.superClass_.disposeInternal.call(this);
  1154. if (this.incTimer_) {
  1155. this.incTimer_.dispose();
  1156. }
  1157. delete this.incTimer_;
  1158. if (this.currentAnimation_) {
  1159. this.currentAnimation_.dispose();
  1160. }
  1161. delete this.currentAnimation_;
  1162. delete this.valueThumb;
  1163. delete this.extentThumb;
  1164. if (this.rangeHighlight) {
  1165. delete this.rangeHighlight;
  1166. }
  1167. this.rangeModel.dispose();
  1168. delete this.rangeModel;
  1169. if (this.keyHandler_) {
  1170. this.keyHandler_.dispose();
  1171. delete this.keyHandler_;
  1172. }
  1173. if (this.mouseWheelHandler_) {
  1174. this.mouseWheelHandler_.dispose();
  1175. delete this.mouseWheelHandler_;
  1176. }
  1177. if (this.valueDragger_) {
  1178. this.valueDragger_.dispose();
  1179. delete this.valueDragger_;
  1180. }
  1181. if (this.extentDragger_) {
  1182. this.extentDragger_.dispose();
  1183. delete this.extentDragger_;
  1184. }
  1185. };
  1186. /**
  1187. * @return {number} The amount to increment/decrement for page up/down as well
  1188. * as when holding down the mouse button on the background.
  1189. */
  1190. goog.ui.SliderBase.prototype.getBlockIncrement = function() {
  1191. return this.blockIncrement_;
  1192. };
  1193. /**
  1194. * Sets the amount to increment/decrement for page up/down as well as when
  1195. * holding down the mouse button on the background.
  1196. *
  1197. * @param {number} value The value to set the block increment to.
  1198. */
  1199. goog.ui.SliderBase.prototype.setBlockIncrement = function(value) {
  1200. this.blockIncrement_ = value;
  1201. };
  1202. /**
  1203. * Sets the minimal value that the extent may have.
  1204. *
  1205. * @param {number} value The minimal value for the extent.
  1206. */
  1207. goog.ui.SliderBase.prototype.setMinExtent = function(value) {
  1208. this.minExtent_ = value;
  1209. };
  1210. /**
  1211. * The amount to increment/decrement for up, down, left and right arrow keys
  1212. * and mouse wheel events.
  1213. * @private
  1214. * @type {number}
  1215. */
  1216. goog.ui.SliderBase.prototype.unitIncrement_ = 1;
  1217. /**
  1218. * @return {number} The amount to increment/decrement for up, down, left and
  1219. * right arrow keys and mouse wheel events.
  1220. */
  1221. goog.ui.SliderBase.prototype.getUnitIncrement = function() {
  1222. return this.unitIncrement_;
  1223. };
  1224. /**
  1225. * Sets the amount to increment/decrement for up, down, left and right arrow
  1226. * keys and mouse wheel events.
  1227. * @param {number} value The value to set the unit increment to.
  1228. */
  1229. goog.ui.SliderBase.prototype.setUnitIncrement = function(value) {
  1230. this.unitIncrement_ = value;
  1231. };
  1232. /**
  1233. * @return {?number} The step value used to determine how to round the value.
  1234. */
  1235. goog.ui.SliderBase.prototype.getStep = function() {
  1236. return this.rangeModel.getStep();
  1237. };
  1238. /**
  1239. * Sets the step value. The step value is used to determine how to round the
  1240. * value.
  1241. * @param {?number} step The step size.
  1242. */
  1243. goog.ui.SliderBase.prototype.setStep = function(step) {
  1244. this.rangeModel.setStep(step);
  1245. };
  1246. /**
  1247. * @return {boolean} Whether clicking on the backgtround should move directly to
  1248. * that point.
  1249. */
  1250. goog.ui.SliderBase.prototype.getMoveToPointEnabled = function() {
  1251. return this.moveToPointEnabled_;
  1252. };
  1253. /**
  1254. * Sets whether clicking on the background should move directly to that point.
  1255. * @param {boolean} val Whether clicking on the background should move directly
  1256. * to that point.
  1257. */
  1258. goog.ui.SliderBase.prototype.setMoveToPointEnabled = function(val) {
  1259. this.moveToPointEnabled_ = val;
  1260. };
  1261. /**
  1262. * @return {number} The value of the underlying range model.
  1263. */
  1264. goog.ui.SliderBase.prototype.getValue = function() {
  1265. return this.rangeModel.getValue();
  1266. };
  1267. /**
  1268. * Sets the value of the underlying range model. We enforce that
  1269. * getMinimum() <= value <= getMaximum() - getExtent()
  1270. * If this is not satisifed for the given value, the call is ignored and no
  1271. * CHANGE event fires.
  1272. * @param {number} value The value.
  1273. */
  1274. goog.ui.SliderBase.prototype.setValue = function(value) {
  1275. // Set the position through the thumb method to enforce constraints.
  1276. this.setThumbPosition_(this.valueThumb, value);
  1277. };
  1278. /**
  1279. * @return {number} The value of the extent of the underlying range model.
  1280. */
  1281. goog.ui.SliderBase.prototype.getExtent = function() {
  1282. return this.rangeModel.getExtent();
  1283. };
  1284. /**
  1285. * Sets the extent of the underlying range model. We enforce that
  1286. * getMinExtent() <= extent <= getMaximum() - getValue()
  1287. * If this is not satisifed for the given extent, the call is ignored and no
  1288. * CHANGE event fires.
  1289. * @param {number} extent The value to which to set the extent.
  1290. */
  1291. goog.ui.SliderBase.prototype.setExtent = function(extent) {
  1292. // Set the position through the thumb method to enforce constraints.
  1293. this.setThumbPosition_(
  1294. this.extentThumb, (this.rangeModel.getValue() + extent));
  1295. };
  1296. /**
  1297. * Change the visibility of the slider.
  1298. * You must call this if you had set the slider's value when it was invisible.
  1299. * @param {boolean} visible Whether to show the slider.
  1300. */
  1301. goog.ui.SliderBase.prototype.setVisible = function(visible) {
  1302. goog.style.setElementShown(this.getElement(), visible);
  1303. if (visible) {
  1304. this.updateUi_();
  1305. }
  1306. };
  1307. /**
  1308. * Set a11y roles and state.
  1309. * @protected
  1310. */
  1311. goog.ui.SliderBase.prototype.setAriaRoles = function() {
  1312. var el = this.getElement();
  1313. goog.asserts.assert(
  1314. el, 'The DOM element for the slider base cannot be null.');
  1315. goog.a11y.aria.setRole(el, goog.a11y.aria.Role.SLIDER);
  1316. this.updateAriaStates();
  1317. };
  1318. /**
  1319. * Set a11y roles and state when values change.
  1320. * @protected
  1321. */
  1322. goog.ui.SliderBase.prototype.updateAriaStates = function() {
  1323. var element = this.getElement();
  1324. if (element) {
  1325. goog.a11y.aria.setState(
  1326. element, goog.a11y.aria.State.VALUEMIN, this.getMinimum());
  1327. goog.a11y.aria.setState(
  1328. element, goog.a11y.aria.State.VALUEMAX, this.getMaximum());
  1329. goog.a11y.aria.setState(
  1330. element, goog.a11y.aria.State.VALUENOW, this.getValue());
  1331. // Passing an empty value to setState will restore the default.
  1332. goog.a11y.aria.setState(
  1333. element, goog.a11y.aria.State.VALUETEXT, this.getTextValue() || '');
  1334. }
  1335. };
  1336. /**
  1337. * Enables or disables mouse wheel handling for the slider. The mouse wheel
  1338. * handler enables the user to change the value of slider using a mouse wheel.
  1339. *
  1340. * @param {boolean} enable Whether to enable mouse wheel handling.
  1341. */
  1342. goog.ui.SliderBase.prototype.setHandleMouseWheel = function(enable) {
  1343. if (this.isInDocument() && enable != this.isHandleMouseWheel()) {
  1344. this.enableMouseWheelHandling_(enable);
  1345. }
  1346. this.isHandleMouseWheel_ = enable;
  1347. };
  1348. /**
  1349. * @return {boolean} Whether the slider handles mousewheel.
  1350. */
  1351. goog.ui.SliderBase.prototype.isHandleMouseWheel = function() {
  1352. return this.isHandleMouseWheel_;
  1353. };
  1354. /**
  1355. * Enable/Disable mouse wheel handling.
  1356. * @param {boolean} enable Whether to enable mouse wheel handling.
  1357. * @private
  1358. */
  1359. goog.ui.SliderBase.prototype.enableMouseWheelHandling_ = function(enable) {
  1360. if (enable) {
  1361. if (!this.mouseWheelHandler_) {
  1362. this.mouseWheelHandler_ =
  1363. new goog.events.MouseWheelHandler(this.getElement());
  1364. }
  1365. this.getHandler().listen(
  1366. this.mouseWheelHandler_,
  1367. goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
  1368. this.handleMouseWheel_);
  1369. } else {
  1370. this.getHandler().unlisten(
  1371. this.mouseWheelHandler_,
  1372. goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
  1373. this.handleMouseWheel_);
  1374. }
  1375. };
  1376. /**
  1377. * Enables or disables the slider. A disabled slider will ignore all
  1378. * user-initiated events. Also fires goog.ui.Component.EventType.ENABLE/DISABLE
  1379. * event as appropriate.
  1380. * @param {boolean} enable Whether to enable the slider or not.
  1381. */
  1382. goog.ui.SliderBase.prototype.setEnabled = function(enable) {
  1383. if (this.enabled_ == enable) {
  1384. return;
  1385. }
  1386. var eventType = enable ? goog.ui.Component.EventType.ENABLE :
  1387. goog.ui.Component.EventType.DISABLE;
  1388. if (this.dispatchEvent(eventType)) {
  1389. this.enabled_ = enable;
  1390. this.enableEventHandlers_(enable);
  1391. if (!enable) {
  1392. // Disabling a slider is equivalent to a mouse up event when the block
  1393. // increment (if happening) should be halted and any possible event
  1394. // handlers be appropriately unlistened.
  1395. this.stopBlockIncrementing_();
  1396. }
  1397. goog.dom.classlist.enable(
  1398. goog.asserts.assert(this.getElement()),
  1399. goog.ui.SliderBase.DISABLED_CSS_CLASS_, !enable);
  1400. }
  1401. };
  1402. /**
  1403. * @return {boolean} Whether the slider is enabled or not.
  1404. */
  1405. goog.ui.SliderBase.prototype.isEnabled = function() {
  1406. return this.enabled_;
  1407. };
  1408. /**
  1409. * @param {Element} element An element for which we want offsetLeft.
  1410. * @return {number} Returns the element's offsetLeft, accounting for RTL if
  1411. * flipForRtl_ is true.
  1412. * @private
  1413. */
  1414. goog.ui.SliderBase.prototype.getOffsetStart_ = function(element) {
  1415. return this.flipForRtl_ ? goog.style.bidi.getOffsetStart(element) :
  1416. element.offsetLeft;
  1417. };
  1418. /**
  1419. * @return {?string} The text value for the slider's current value, or null if
  1420. * unavailable.
  1421. */
  1422. goog.ui.SliderBase.prototype.getTextValue = function() {
  1423. return this.labelFn_(this.getValue());
  1424. };
  1425. /**
  1426. * The factory for creating additional animations to be played when animating to
  1427. * a new value.
  1428. * @interface
  1429. */
  1430. goog.ui.SliderBase.AnimationFactory = function() {};
  1431. /**
  1432. * Creates an additional animation to play when animating to a new value.
  1433. *
  1434. * @param {number} previousValue The previous value (before animation).
  1435. * @param {number} newValue The new value (after animation).
  1436. * @param {number} interval The animation interval.
  1437. * @return {!Array<!goog.fx.TransitionBase>} The additional animations to play.
  1438. */
  1439. goog.ui.SliderBase.AnimationFactory.prototype.createAnimations;