scrollbar.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2011 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Library for creating scrollbars.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.Scrollbar');
  26. goog.provide('Blockly.ScrollbarPair');
  27. goog.require('goog.dom');
  28. goog.require('goog.events');
  29. /**
  30. * Class for a pair of scrollbars. Horizontal and vertical.
  31. * @param {!Blockly.Workspace} workspace Workspace to bind the scrollbars to.
  32. * @constructor
  33. */
  34. Blockly.ScrollbarPair = function(workspace) {
  35. this.workspace_ = workspace;
  36. this.hScroll = new Blockly.Scrollbar(workspace, true, true);
  37. this.vScroll = new Blockly.Scrollbar(workspace, false, true);
  38. this.corner_ = Blockly.createSvgElement('rect',
  39. {'height': Blockly.Scrollbar.scrollbarThickness,
  40. 'width': Blockly.Scrollbar.scrollbarThickness,
  41. 'class': 'blocklyScrollbarBackground'}, null);
  42. Blockly.Scrollbar.insertAfter_(this.corner_, workspace.getBubbleCanvas());
  43. };
  44. /**
  45. * Previously recorded metrics from the workspace.
  46. * @type {Object}
  47. * @private
  48. */
  49. Blockly.ScrollbarPair.prototype.oldHostMetrics_ = null;
  50. /**
  51. * Dispose of this pair of scrollbars.
  52. * Unlink from all DOM elements to prevent memory leaks.
  53. */
  54. Blockly.ScrollbarPair.prototype.dispose = function() {
  55. goog.dom.removeNode(this.corner_);
  56. this.corner_ = null;
  57. this.workspace_ = null;
  58. this.oldHostMetrics_ = null;
  59. this.hScroll.dispose();
  60. this.hScroll = null;
  61. this.vScroll.dispose();
  62. this.vScroll = null;
  63. };
  64. /**
  65. * Recalculate both of the scrollbars' locations and lengths.
  66. * Also reposition the corner rectangle.
  67. */
  68. Blockly.ScrollbarPair.prototype.resize = function() {
  69. // Look up the host metrics once, and use for both scrollbars.
  70. var hostMetrics = this.workspace_.getMetrics();
  71. if (!hostMetrics) {
  72. // Host element is likely not visible.
  73. return;
  74. }
  75. // Only change the scrollbars if there has been a change in metrics.
  76. var resizeH = false;
  77. var resizeV = false;
  78. if (!this.oldHostMetrics_ ||
  79. this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth ||
  80. this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight ||
  81. this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop ||
  82. this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) {
  83. // The window has been resized or repositioned.
  84. resizeH = true;
  85. resizeV = true;
  86. } else {
  87. // Has the content been resized or moved?
  88. if (!this.oldHostMetrics_ ||
  89. this.oldHostMetrics_.contentWidth != hostMetrics.contentWidth ||
  90. this.oldHostMetrics_.viewLeft != hostMetrics.viewLeft ||
  91. this.oldHostMetrics_.contentLeft != hostMetrics.contentLeft) {
  92. resizeH = true;
  93. }
  94. if (!this.oldHostMetrics_ ||
  95. this.oldHostMetrics_.contentHeight != hostMetrics.contentHeight ||
  96. this.oldHostMetrics_.viewTop != hostMetrics.viewTop ||
  97. this.oldHostMetrics_.contentTop != hostMetrics.contentTop) {
  98. resizeV = true;
  99. }
  100. }
  101. if (resizeH) {
  102. this.hScroll.resize(hostMetrics);
  103. }
  104. if (resizeV) {
  105. this.vScroll.resize(hostMetrics);
  106. }
  107. // Reposition the corner square.
  108. if (!this.oldHostMetrics_ ||
  109. this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth ||
  110. this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) {
  111. this.corner_.setAttribute('x', this.vScroll.position_.x);
  112. }
  113. if (!this.oldHostMetrics_ ||
  114. this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight ||
  115. this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop) {
  116. this.corner_.setAttribute('y', this.hScroll.position_.y);
  117. }
  118. // Cache the current metrics to potentially short-cut the next resize event.
  119. this.oldHostMetrics_ = hostMetrics;
  120. };
  121. /**
  122. * Set the sliders of both scrollbars to be at a certain position.
  123. * @param {number} x Horizontal scroll value.
  124. * @param {number} y Vertical scroll value.
  125. */
  126. Blockly.ScrollbarPair.prototype.set = function(x, y) {
  127. // This function is equivalent to:
  128. // this.hScroll.set(x);
  129. // this.vScroll.set(y);
  130. // However, that calls setMetrics twice which causes a chain of
  131. // getAttribute->setAttribute->getAttribute resulting in an extra layout pass.
  132. // Combining them speeds up rendering.
  133. var xyRatio = {};
  134. var hHandlePosition = x * this.hScroll.ratio_;
  135. var vHandlePosition = y * this.vScroll.ratio_;
  136. var hBarLength = this.hScroll.scrollViewSize_;
  137. var vBarLength = this.vScroll.scrollViewSize_;
  138. xyRatio.x = this.getRatio_(hHandlePosition, hBarLength);
  139. xyRatio.y = this.getRatio_(vHandlePosition, vBarLength);
  140. this.workspace_.setMetrics(xyRatio);
  141. this.hScroll.setHandlePosition(hHandlePosition);
  142. this.vScroll.setHandlePosition(vHandlePosition);
  143. };
  144. /**
  145. * Helper to calculate the ratio of handle position to scrollbar view size.
  146. * @param {number} handlePosition The value of the handle.
  147. * @param {number} viewSize The total size of the scrollbar's view.
  148. * @return {number} Ratio.
  149. * @private
  150. */
  151. Blockly.ScrollbarPair.prototype.getRatio_ = function(handlePosition, viewSize) {
  152. var ratio = handlePosition / viewSize;
  153. if (isNaN(ratio)) {
  154. return 0;
  155. }
  156. return ratio;
  157. };
  158. // --------------------------------------------------------------------
  159. /**
  160. * Class for a pure SVG scrollbar.
  161. * This technique offers a scrollbar that is guaranteed to work, but may not
  162. * look or behave like the system's scrollbars.
  163. * @param {!Blockly.Workspace} workspace Workspace to bind the scrollbar to.
  164. * @param {boolean} horizontal True if horizontal, false if vertical.
  165. * @param {boolean=} opt_pair True if scrollbar is part of a horiz/vert pair.
  166. * @constructor
  167. */
  168. Blockly.Scrollbar = function(workspace, horizontal, opt_pair) {
  169. this.workspace_ = workspace;
  170. this.pair_ = opt_pair || false;
  171. this.horizontal_ = horizontal;
  172. this.oldHostMetrics_ = null;
  173. this.createDom_();
  174. /**
  175. * The upper left corner of the scrollbar's svg group.
  176. * @type {goog.math.Coordinate}
  177. * @private
  178. */
  179. this.position_ = new goog.math.Coordinate(0, 0);
  180. if (horizontal) {
  181. this.svgBackground_.setAttribute('height',
  182. Blockly.Scrollbar.scrollbarThickness);
  183. this.svgHandle_.setAttribute('height',
  184. Blockly.Scrollbar.scrollbarThickness - 5);
  185. this.svgHandle_.setAttribute('y', 2.5);
  186. this.lengthAttribute_ = 'width';
  187. this.positionAttribute_ = 'x';
  188. } else {
  189. this.svgBackground_.setAttribute('width',
  190. Blockly.Scrollbar.scrollbarThickness);
  191. this.svgHandle_.setAttribute('width',
  192. Blockly.Scrollbar.scrollbarThickness - 5);
  193. this.svgHandle_.setAttribute('x', 2.5);
  194. this.lengthAttribute_ = 'height';
  195. this.positionAttribute_ = 'y';
  196. }
  197. var scrollbar = this;
  198. this.onMouseDownBarWrapper_ = Blockly.bindEventWithChecks_(
  199. this.svgBackground_, 'mousedown', scrollbar, scrollbar.onMouseDownBar_);
  200. this.onMouseDownHandleWrapper_ = Blockly.bindEventWithChecks_(this.svgHandle_,
  201. 'mousedown', scrollbar, scrollbar.onMouseDownHandle_);
  202. };
  203. /**
  204. * The size of the area within which the scrollbar handle can move.
  205. * @type {number}
  206. * @private
  207. */
  208. Blockly.Scrollbar.prototype.scrollViewSize_ = 0;
  209. /**
  210. * The length of the scrollbar handle.
  211. * @type {number}
  212. * @private
  213. */
  214. Blockly.Scrollbar.prototype.handleLength_ = 0;
  215. /**
  216. * The offset of the start of the handle from the start of the scrollbar range.
  217. * @type {number}
  218. * @private
  219. */
  220. Blockly.Scrollbar.prototype.handlePosition_ = 0;
  221. /**
  222. * Whether the scrollbar handle is visible.
  223. * @type {boolean}
  224. * @private
  225. */
  226. Blockly.Scrollbar.prototype.isVisible_ = true;
  227. /**
  228. * Width of vertical scrollbar or height of horizontal scrollbar.
  229. * Increase the size of scrollbars on touch devices.
  230. * Don't define if there is no document object (e.g. node.js).
  231. */
  232. Blockly.Scrollbar.scrollbarThickness = 15;
  233. if (goog.events.BrowserFeature.TOUCH_ENABLED) {
  234. Blockly.Scrollbar.scrollbarThickness = 25;
  235. }
  236. /**
  237. * @param {!Object} first An object containing computed measurements of a
  238. * workspace.
  239. * @param {!Object} second Another object containing computed measurements of a
  240. * workspace.
  241. * @return {boolean} Whether the two sets of metrics are equivalent.
  242. * @private
  243. */
  244. Blockly.Scrollbar.metricsAreEquivalent_ = function(first, second) {
  245. if (!(first && second)) {
  246. return false;
  247. }
  248. if (first.viewWidth != second.viewWidth ||
  249. first.viewHeight != second.viewHeight ||
  250. first.viewLeft != second.viewLeft ||
  251. first.viewTop != second.viewTop ||
  252. first.absoluteTop != second.absoluteTop ||
  253. first.absoluteLeft != second.absoluteLeft ||
  254. first.contentWidth != second.contentWidth ||
  255. first.contentHeight != second.contentHeight ||
  256. first.contentLeft != second.contentLeft ||
  257. first.contentTop != second.contentTop) {
  258. return false;
  259. }
  260. return true;
  261. };
  262. /**
  263. * Dispose of this scrollbar.
  264. * Unlink from all DOM elements to prevent memory leaks.
  265. */
  266. Blockly.Scrollbar.prototype.dispose = function() {
  267. this.cleanUp_();
  268. Blockly.unbindEvent_(this.onMouseDownBarWrapper_);
  269. this.onMouseDownBarWrapper_ = null;
  270. Blockly.unbindEvent_(this.onMouseDownHandleWrapper_);
  271. this.onMouseDownHandleWrapper_ = null;
  272. goog.dom.removeNode(this.svgGroup_);
  273. this.svgGroup_ = null;
  274. this.svgBackground_ = null;
  275. this.svgHandle_ = null;
  276. this.workspace_ = null;
  277. };
  278. /**
  279. * Set the length of the scrollbar's handle and change the SVG attribute
  280. * accordingly.
  281. * @param {number} newLength The new scrollbar handle length.
  282. */
  283. Blockly.Scrollbar.prototype.setHandleLength_ = function(newLength) {
  284. this.handleLength_ = newLength;
  285. this.svgHandle_.setAttribute(this.lengthAttribute_, this.handleLength_);
  286. };
  287. /**
  288. * Set the offset of the scrollbar's handle and change the SVG attribute
  289. * accordingly.
  290. * @param {number} newPosition The new scrollbar handle offset.
  291. */
  292. Blockly.Scrollbar.prototype.setHandlePosition = function(newPosition) {
  293. this.handlePosition_ = newPosition;
  294. this.svgHandle_.setAttribute(this.positionAttribute_, this.handlePosition_);
  295. };
  296. /**
  297. * Set the size of the scrollbar's background and change the SVG attribute
  298. * accordingly.
  299. * @param {number} newSize The new scrollbar background length.
  300. * @private
  301. */
  302. Blockly.Scrollbar.prototype.setScrollViewSize_ = function(newSize) {
  303. this.scrollViewSize_ = newSize;
  304. this.svgBackground_.setAttribute(this.lengthAttribute_, this.scrollViewSize_);
  305. };
  306. /**
  307. * Set the position of the scrollbar's svg group.
  308. * @param {number} x The new x coordinate.
  309. * @param {number} y The new y coordinate.
  310. */
  311. Blockly.Scrollbar.prototype.setPosition = function(x, y) {
  312. this.position_.x = x;
  313. this.position_.y = y;
  314. this.svgGroup_.setAttribute('transform',
  315. 'translate(' + this.position_.x + ',' + this.position_.y + ')');
  316. };
  317. /**
  318. * Recalculate the scrollbar's location and its length.
  319. * @param {Object=} opt_metrics A data structure of from the describing all the
  320. * required dimensions. If not provided, it will be fetched from the host
  321. * object.
  322. */
  323. Blockly.Scrollbar.prototype.resize = function(opt_metrics) {
  324. // Determine the location, height and width of the host element.
  325. var hostMetrics = opt_metrics;
  326. if (!hostMetrics) {
  327. hostMetrics = this.workspace_.getMetrics();
  328. if (!hostMetrics) {
  329. // Host element is likely not visible.
  330. return;
  331. }
  332. }
  333. if (Blockly.Scrollbar.metricsAreEquivalent_(hostMetrics,
  334. this.oldHostMetrics_)) {
  335. return;
  336. }
  337. this.oldHostMetrics_ = hostMetrics;
  338. /* hostMetrics is an object with the following properties.
  339. * .viewHeight: Height of the visible rectangle,
  340. * .viewWidth: Width of the visible rectangle,
  341. * .contentHeight: Height of the contents,
  342. * .contentWidth: Width of the content,
  343. * .viewTop: Offset of top edge of visible rectangle from parent,
  344. * .viewLeft: Offset of left edge of visible rectangle from parent,
  345. * .contentTop: Offset of the top-most content from the y=0 coordinate,
  346. * .contentLeft: Offset of the left-most content from the x=0 coordinate,
  347. * .absoluteTop: Top-edge of view.
  348. * .absoluteLeft: Left-edge of view.
  349. */
  350. if (this.horizontal_) {
  351. this.resizeHorizontal_(hostMetrics);
  352. } else {
  353. this.resizeVertical_(hostMetrics);
  354. }
  355. // Resizing may have caused some scrolling.
  356. this.onScroll_();
  357. };
  358. /**
  359. * Recalculate a horizontal scrollbar's location and length.
  360. * @param {!Object} hostMetrics A data structure describing all the
  361. * required dimensions, possibly fetched from the host object.
  362. * @private
  363. */
  364. Blockly.Scrollbar.prototype.resizeHorizontal_ = function(hostMetrics) {
  365. // TODO: Inspect metrics to determine if we can get away with just a content
  366. // resize.
  367. this.resizeViewHorizontal(hostMetrics);
  368. };
  369. /**
  370. * Recalculate a horizontal scrollbar's location on the screen and path length.
  371. * This should be called when the layout or size of the window has changed.
  372. * @param {!Object} hostMetrics A data structure describing all the
  373. * required dimensions, possibly fetched from the host object.
  374. */
  375. Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) {
  376. var viewSize = hostMetrics.viewWidth - 1;
  377. if (this.pair_) {
  378. // Shorten the scrollbar to make room for the corner square.
  379. viewSize -= Blockly.Scrollbar.scrollbarThickness;
  380. }
  381. this.setScrollViewSize_(Math.max(0, viewSize));
  382. var xCoordinate = hostMetrics.absoluteLeft + 0.5;
  383. if (this.pair_ && this.workspace_.RTL) {
  384. xCoordinate += Blockly.Scrollbar.scrollbarThickness;
  385. }
  386. // Horizontal toolbar should always be just above the bottom of the workspace.
  387. var yCoordinate = hostMetrics.absoluteTop + hostMetrics.viewHeight -
  388. Blockly.Scrollbar.scrollbarThickness - 0.5;
  389. this.setPosition(xCoordinate, yCoordinate);
  390. // If the view has been resized, a content resize will also be necessary. The
  391. // reverse is not true.
  392. this.resizeContentHorizontal(hostMetrics);
  393. };
  394. /**
  395. * Recalculate a horizontal scrollbar's location within its path and length.
  396. * This should be called when the contents of the workspace have changed.
  397. * @param {!Object} hostMetrics A data structure describing all the
  398. * required dimensions, possibly fetched from the host object.
  399. */
  400. Blockly.Scrollbar.prototype.resizeContentHorizontal = function(hostMetrics) {
  401. if (!this.pair_) {
  402. // Only show the scrollbar if needed.
  403. // Ideally this would also apply to scrollbar pairs, but that's a bigger
  404. // headache (due to interactions with the corner square).
  405. this.setVisible(this.scrollViewSize_ < hostMetrics.contentWidth);
  406. }
  407. this.ratio_ = this.scrollViewSize_ / hostMetrics.contentWidth;
  408. if (this.ratio_ == -Infinity || this.ratio_ == Infinity ||
  409. isNaN(this.ratio_)) {
  410. this.ratio_ = 0;
  411. }
  412. var handleLength = hostMetrics.viewWidth * this.ratio_;
  413. this.setHandleLength_(Math.max(0, handleLength));
  414. var handlePosition = (hostMetrics.viewLeft - hostMetrics.contentLeft) *
  415. this.ratio_;
  416. this.setHandlePosition(this.constrainHandle_(handlePosition));
  417. };
  418. /**
  419. * Recalculate a vertical scrollbar's location and length.
  420. * @param {!Object} hostMetrics A data structure describing all the
  421. * required dimensions, possibly fetched from the host object.
  422. * @private
  423. */
  424. Blockly.Scrollbar.prototype.resizeVertical_ = function(hostMetrics) {
  425. // TODO: Inspect metrics to determine if we can get away with just a content
  426. // resize.
  427. this.resizeViewVertical(hostMetrics);
  428. };
  429. /**
  430. * Recalculate a vertical scrollbar's location on the screen and path length.
  431. * This should be called when the layout or size of the window has changed.
  432. * @param {!Object} hostMetrics A data structure describing all the
  433. * required dimensions, possibly fetched from the host object.
  434. */
  435. Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) {
  436. var viewSize = hostMetrics.viewHeight - 1;
  437. if (this.pair_) {
  438. // Shorten the scrollbar to make room for the corner square.
  439. viewSize -= Blockly.Scrollbar.scrollbarThickness;
  440. }
  441. this.setScrollViewSize_(Math.max(0, viewSize));
  442. var xCoordinate = hostMetrics.absoluteLeft + 0.5;
  443. if (!this.workspace_.RTL) {
  444. xCoordinate += hostMetrics.viewWidth -
  445. Blockly.Scrollbar.scrollbarThickness - 1;
  446. }
  447. var yCoordinate = hostMetrics.absoluteTop + 0.5;
  448. this.setPosition(xCoordinate, yCoordinate);
  449. // If the view has been resized, a content resize will also be necessary. The
  450. // reverse is not true.
  451. this.resizeContentVertical(hostMetrics);
  452. };
  453. /**
  454. * Recalculate a vertical scrollbar's location within its path and length.
  455. * This should be called when the contents of the workspace have changed.
  456. * @param {!Object} hostMetrics A data structure describing all the
  457. * required dimensions, possibly fetched from the host object.
  458. */
  459. Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) {
  460. if (!this.pair_) {
  461. // Only show the scrollbar if needed.
  462. this.setVisible(this.scrollViewSize_ < hostMetrics.contentHeight);
  463. }
  464. this.ratio_ = this.scrollViewSize_ / hostMetrics.contentHeight;
  465. if (this.ratio_ == -Infinity || this.ratio_ == Infinity ||
  466. isNaN(this.ratio_)) {
  467. this.ratio_ = 0;
  468. }
  469. var handleLength = hostMetrics.viewHeight * this.ratio_;
  470. this.setHandleLength_(Math.max(0, handleLength));
  471. var handlePosition = (hostMetrics.viewTop - hostMetrics.contentTop) *
  472. this.ratio_;
  473. this.setHandlePosition(this.constrainHandle_(handlePosition));
  474. };
  475. /**
  476. * Create all the DOM elements required for a scrollbar.
  477. * The resulting widget is not sized.
  478. * @private
  479. */
  480. Blockly.Scrollbar.prototype.createDom_ = function() {
  481. /* Create the following DOM:
  482. <g class="blocklyScrollbarHorizontal">
  483. <rect class="blocklyScrollbarBackground" />
  484. <rect class="blocklyScrollbarHandle" rx="8" ry="8" />
  485. </g>
  486. */
  487. var className = 'blocklyScrollbar' +
  488. (this.horizontal_ ? 'Horizontal' : 'Vertical');
  489. this.svgGroup_ = Blockly.createSvgElement('g', {'class': className}, null);
  490. this.svgBackground_ = Blockly.createSvgElement('rect',
  491. {'class': 'blocklyScrollbarBackground'}, this.svgGroup_);
  492. var radius = Math.floor((Blockly.Scrollbar.scrollbarThickness - 5) / 2);
  493. this.svgHandle_ = Blockly.createSvgElement('rect',
  494. {'class': 'blocklyScrollbarHandle', 'rx': radius, 'ry': radius},
  495. this.svgGroup_);
  496. Blockly.Scrollbar.insertAfter_(this.svgGroup_,
  497. this.workspace_.getBubbleCanvas());
  498. };
  499. /**
  500. * Is the scrollbar visible. Non-paired scrollbars disappear when they aren't
  501. * needed.
  502. * @return {boolean} True if visible.
  503. */
  504. Blockly.Scrollbar.prototype.isVisible = function() {
  505. return this.isVisible_;
  506. };
  507. /**
  508. * Set whether the scrollbar is visible.
  509. * Only applies to non-paired scrollbars.
  510. * @param {boolean} visible True if visible.
  511. */
  512. Blockly.Scrollbar.prototype.setVisible = function(visible) {
  513. if (visible == this.isVisible()) {
  514. return;
  515. }
  516. // Ideally this would also apply to scrollbar pairs, but that's a bigger
  517. // headache (due to interactions with the corner square).
  518. if (this.pair_) {
  519. throw 'Unable to toggle visibility of paired scrollbars.';
  520. }
  521. this.isVisible_ = visible;
  522. if (visible) {
  523. this.svgGroup_.setAttribute('display', 'block');
  524. } else {
  525. // Hide the scrollbar.
  526. this.workspace_.setMetrics({x: 0, y: 0});
  527. this.svgGroup_.setAttribute('display', 'none');
  528. }
  529. };
  530. /**
  531. * Scroll by one pageful.
  532. * Called when scrollbar background is clicked.
  533. * @param {!Event} e Mouse down event.
  534. * @private
  535. */
  536. Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) {
  537. this.workspace_.markFocused();
  538. Blockly.Touch.clearTouchIdentifier(); // This is really a click.
  539. this.cleanUp_();
  540. if (Blockly.isRightButton(e)) {
  541. // Right-click.
  542. // Scrollbars have no context menu.
  543. e.stopPropagation();
  544. return;
  545. }
  546. var mouseXY = Blockly.mouseToSvg(e, this.workspace_.getParentSvg(),
  547. this.workspace_.getInverseScreenCTM());
  548. var mouseLocation = this.horizontal_ ? mouseXY.x : mouseXY.y;
  549. var handleXY = Blockly.getSvgXY_(this.svgHandle_, this.workspace_);
  550. var handleStart = this.horizontal_ ? handleXY.x : handleXY.y;
  551. var handlePosition = this.handlePosition_;
  552. var pageLength = this.handleLength_ * 0.95;
  553. if (mouseLocation <= handleStart) {
  554. // Decrease the scrollbar's value by a page.
  555. handlePosition -= pageLength;
  556. } else if (mouseLocation >= handleStart + this.handleLength_) {
  557. // Increase the scrollbar's value by a page.
  558. handlePosition += pageLength;
  559. }
  560. this.setHandlePosition(this.constrainHandle_(handlePosition));
  561. this.onScroll_();
  562. e.stopPropagation();
  563. e.preventDefault();
  564. };
  565. /**
  566. * Start a dragging operation.
  567. * Called when scrollbar handle is clicked.
  568. * @param {!Event} e Mouse down event.
  569. * @private
  570. */
  571. Blockly.Scrollbar.prototype.onMouseDownHandle_ = function(e) {
  572. this.workspace_.markFocused();
  573. this.cleanUp_();
  574. if (Blockly.isRightButton(e)) {
  575. // Right-click.
  576. // Scrollbars have no context menu.
  577. e.stopPropagation();
  578. return;
  579. }
  580. // Look up the current translation and record it.
  581. this.startDragHandle = this.handlePosition_;
  582. // Record the current mouse position.
  583. this.startDragMouse = this.horizontal_ ? e.clientX : e.clientY;
  584. Blockly.Scrollbar.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document,
  585. 'mouseup', this, this.onMouseUpHandle_);
  586. Blockly.Scrollbar.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(document,
  587. 'mousemove', this, this.onMouseMoveHandle_);
  588. e.stopPropagation();
  589. e.preventDefault();
  590. };
  591. /**
  592. * Drag the scrollbar's handle.
  593. * @param {!Event} e Mouse up event.
  594. * @private
  595. */
  596. Blockly.Scrollbar.prototype.onMouseMoveHandle_ = function(e) {
  597. var currentMouse = this.horizontal_ ? e.clientX : e.clientY;
  598. var mouseDelta = currentMouse - this.startDragMouse;
  599. var handlePosition = this.startDragHandle + mouseDelta;
  600. // Position the bar.
  601. this.setHandlePosition(this.constrainHandle_(handlePosition));
  602. this.onScroll_();
  603. };
  604. /**
  605. * Release the scrollbar handle and reset state accordingly.
  606. * @private
  607. */
  608. Blockly.Scrollbar.prototype.onMouseUpHandle_ = function() {
  609. Blockly.Touch.clearTouchIdentifier();
  610. this.cleanUp_();
  611. };
  612. /**
  613. * Hide chaff and stop binding to mouseup and mousemove events. Call this to
  614. * wrap up lose ends associated with the scrollbar.
  615. * @private
  616. */
  617. Blockly.Scrollbar.prototype.cleanUp_ = function() {
  618. Blockly.hideChaff(true);
  619. if (Blockly.Scrollbar.onMouseUpWrapper_) {
  620. Blockly.unbindEvent_(Blockly.Scrollbar.onMouseUpWrapper_);
  621. Blockly.Scrollbar.onMouseUpWrapper_ = null;
  622. }
  623. if (Blockly.Scrollbar.onMouseMoveWrapper_) {
  624. Blockly.unbindEvent_(Blockly.Scrollbar.onMouseMoveWrapper_);
  625. Blockly.Scrollbar.onMouseMoveWrapper_ = null;
  626. }
  627. };
  628. /**
  629. * Constrain the handle's position within the minimum (0) and maximum
  630. * (length of scrollbar) values allowed for the scrollbar.
  631. * @param {number} value Value that is potentially out of bounds.
  632. * @return {number} Constrained value.
  633. * @private
  634. */
  635. Blockly.Scrollbar.prototype.constrainHandle_ = function(value) {
  636. if (value <= 0 || isNaN(value) || this.scrollViewSize_ < this.handleLength_) {
  637. value = 0;
  638. } else {
  639. value = Math.min(value, this.scrollViewSize_ - this.handleLength_);
  640. }
  641. return value;
  642. };
  643. /**
  644. * Called when scrollbar is moved.
  645. * @private
  646. */
  647. Blockly.Scrollbar.prototype.onScroll_ = function() {
  648. var ratio = this.handlePosition_ / this.scrollViewSize_;
  649. if (isNaN(ratio)) {
  650. ratio = 0;
  651. }
  652. var xyRatio = {};
  653. if (this.horizontal_) {
  654. xyRatio.x = ratio;
  655. } else {
  656. xyRatio.y = ratio;
  657. }
  658. this.workspace_.setMetrics(xyRatio);
  659. };
  660. /**
  661. * Set the scrollbar slider's position.
  662. * @param {number} value The distance from the top/left end of the bar.
  663. */
  664. Blockly.Scrollbar.prototype.set = function(value) {
  665. this.setHandlePosition(this.constrainHandle_(value * this.ratio_));
  666. this.onScroll_();
  667. };
  668. /**
  669. * Insert a node after a reference node.
  670. * Contrast with node.insertBefore function.
  671. * @param {!Element} newNode New element to insert.
  672. * @param {!Element} refNode Existing element to precede new node.
  673. * @private
  674. */
  675. Blockly.Scrollbar.insertAfter_ = function(newNode, refNode) {
  676. var siblingNode = refNode.nextSibling;
  677. var parentNode = refNode.parentNode;
  678. if (!parentNode) {
  679. throw 'Reference node has no parent.';
  680. }
  681. if (siblingNode) {
  682. parentNode.insertBefore(newNode, siblingNode);
  683. } else {
  684. parentNode.appendChild(newNode);
  685. }
  686. };