ui.scrollable.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /**
  2. * DevExtreme (ui/scroll_view/ui.scrollable.js)
  3. * Version: 19.1.16
  4. * Build date: Tue Oct 18 2022
  5. *
  6. * Copyright (c) 2012 - 2022 Developer Express Inc. ALL RIGHTS RESERVED
  7. * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
  8. */
  9. "use strict";
  10. var _renderer = require("../../core/renderer");
  11. var _renderer2 = _interopRequireDefault(_renderer);
  12. var _events_engine = require("../../events/core/events_engine");
  13. var _events_engine2 = _interopRequireDefault(_events_engine);
  14. var _support = require("../../core/utils/support");
  15. var _support2 = _interopRequireDefault(_support);
  16. var _browser = require("../../core/utils/browser");
  17. var _browser2 = _interopRequireDefault(_browser);
  18. var _common = require("../../core/utils/common");
  19. var _common2 = _interopRequireDefault(_common);
  20. var _type = require("../../core/utils/type");
  21. var _type2 = _interopRequireDefault(_type);
  22. var _extend = require("../../core/utils/extend");
  23. var _dom = require("../../core/utils/dom");
  24. var _window = require("../../core/utils/window");
  25. var _window2 = _interopRequireDefault(_window);
  26. var _dom_adapter = require("../../core/dom_adapter");
  27. var _dom_adapter2 = _interopRequireDefault(_dom_adapter);
  28. var _devices = require("../../core/devices");
  29. var _devices2 = _interopRequireDefault(_devices);
  30. var _component_registrator = require("../../core/component_registrator");
  31. var _component_registrator2 = _interopRequireDefault(_component_registrator);
  32. var _dom_component = require("../../core/dom_component");
  33. var _dom_component2 = _interopRequireDefault(_dom_component);
  34. var _selectors = require("../widget/selectors");
  35. var _selectors2 = _interopRequireDefault(_selectors);
  36. var _utils = require("../../events/utils");
  37. var _utils2 = _interopRequireDefault(_utils);
  38. var _uiEventsEmitterGesture = require("./ui.events.emitter.gesture.scroll");
  39. var _uiEventsEmitterGesture2 = _interopRequireDefault(_uiEventsEmitterGesture);
  40. var _uiScrollable = require("./ui.scrollable.simulated");
  41. var _uiScrollable2 = _interopRequireDefault(_uiScrollable);
  42. var _uiScrollable3 = require("./ui.scrollable.native");
  43. var _uiScrollable4 = _interopRequireDefault(_uiScrollable3);
  44. var _deferred = require("../../core/utils/deferred");
  45. function _interopRequireDefault(obj) {
  46. return obj && obj.__esModule ? obj : {
  47. "default": obj
  48. }
  49. }
  50. var SCROLLABLE = "dxScrollable";
  51. var SCROLLABLE_STRATEGY = "dxScrollableStrategy";
  52. var SCROLLABLE_CLASS = "dx-scrollable";
  53. var SCROLLABLE_DISABLED_CLASS = "dx-scrollable-disabled";
  54. var SCROLLABLE_CONTAINER_CLASS = "dx-scrollable-container";
  55. var SCROLLABLE_WRAPPER_CLASS = "dx-scrollable-wrapper";
  56. var SCROLLABLE_CONTENT_CLASS = "dx-scrollable-content";
  57. var SCROLLABLE_CUSTOMIZABLE_SCROLLBARS_CLASS = "dx-scrollable-customizable-scrollbars";
  58. var VERTICAL = "vertical";
  59. var HORIZONTAL = "horizontal";
  60. var BOTH = "both";
  61. var deviceDependentOptions = function() {
  62. return [{
  63. device: function() {
  64. return !_support2.default.nativeScrolling
  65. },
  66. options: {
  67. useNative: false
  68. }
  69. }, {
  70. device: function(_device) {
  71. return !_devices2.default.isSimulator() && "generic" === _devices2.default.real().platform && "generic" === _device.platform
  72. },
  73. options: {
  74. bounceEnabled: false,
  75. scrollByThumb: true,
  76. scrollByContent: _support2.default.touch,
  77. showScrollbar: "onHover"
  78. }
  79. }]
  80. };
  81. var Scrollable = _dom_component2.default.inherit({
  82. _getDefaultOptions: function() {
  83. return (0, _extend.extend)(this.callBase(), {
  84. disabled: false,
  85. onScroll: null,
  86. direction: VERTICAL,
  87. showScrollbar: "onScroll",
  88. useNative: true,
  89. bounceEnabled: true,
  90. scrollByContent: true,
  91. scrollByThumb: false,
  92. onUpdated: null,
  93. onStart: null,
  94. onEnd: null,
  95. onBounce: null,
  96. onStop: null,
  97. useSimulatedScrollbar: false,
  98. useKeyboard: true,
  99. inertiaEnabled: true,
  100. pushBackValue: 0,
  101. updateManually: false
  102. })
  103. },
  104. _defaultOptionsRules: function() {
  105. return this.callBase().concat(deviceDependentOptions(), [{
  106. device: function() {
  107. return _support2.default.nativeScrolling && "android" === _devices2.default.real().platform && !_browser2.default.mozilla
  108. },
  109. options: {
  110. useSimulatedScrollbar: true
  111. }
  112. }, {
  113. device: function() {
  114. return "ios" === _devices2.default.real().platform
  115. },
  116. options: {
  117. pushBackValue: 1
  118. }
  119. }])
  120. },
  121. _initOptions: function(options) {
  122. this.callBase(options);
  123. if (!("useSimulatedScrollbar" in options)) {
  124. this._setUseSimulatedScrollbar()
  125. }
  126. },
  127. _setUseSimulatedScrollbar: function() {
  128. if (!this.initialOption("useSimulatedScrollbar")) {
  129. this.option("useSimulatedScrollbar", !this.option("useNative"))
  130. }
  131. },
  132. _init: function() {
  133. this.callBase();
  134. this._initScrollableMarkup();
  135. this._locked = false
  136. },
  137. _visibilityChanged: function(visible) {
  138. if (visible) {
  139. this.update();
  140. this._updateRtlPosition();
  141. this._savedScrollOffset && this.scrollTo(this._savedScrollOffset);
  142. delete this._savedScrollOffset
  143. } else {
  144. this._savedScrollOffset = this.scrollOffset()
  145. }
  146. },
  147. _initScrollableMarkup: function() {
  148. var $element = this.$element().addClass(SCROLLABLE_CLASS);
  149. var $container = this._$container = (0, _renderer2.default)("<div>").addClass(SCROLLABLE_CONTAINER_CLASS);
  150. var $wrapper = this._$wrapper = (0, _renderer2.default)("<div>").addClass(SCROLLABLE_WRAPPER_CLASS);
  151. var $content = this._$content = (0, _renderer2.default)("<div>").addClass(SCROLLABLE_CONTENT_CLASS);
  152. if (_dom_adapter2.default.hasDocumentProperty("onbeforeactivate") && _browser2.default.msie && _browser2.default.version < 12) {
  153. _events_engine2.default.on($element, _utils2.default.addNamespace("beforeactivate", SCROLLABLE), function(e) {
  154. if (!(0, _renderer2.default)(e.target).is(_selectors2.default.focusable)) {
  155. e.preventDefault()
  156. }
  157. })
  158. }
  159. $content.append($element.contents()).appendTo($container);
  160. $container.appendTo($wrapper);
  161. $wrapper.appendTo($element)
  162. },
  163. _dimensionChanged: function() {
  164. this.update()
  165. },
  166. _attachNativeScrollbarsCustomizationCss: function() {
  167. if ("desktop" === _devices2.default.real().deviceType && !(_window2.default.getNavigator().platform.indexOf("Mac") > -1 && _browser2.default.webkit)) {
  168. this.$element().addClass(SCROLLABLE_CUSTOMIZABLE_SCROLLBARS_CLASS)
  169. }
  170. },
  171. _initMarkup: function() {
  172. this.callBase();
  173. this._renderDirection()
  174. },
  175. _render: function() {
  176. this._renderStrategy();
  177. this._attachNativeScrollbarsCustomizationCss();
  178. this._attachEventHandlers();
  179. this._renderDisabledState();
  180. this._createActions();
  181. this.update();
  182. this.callBase();
  183. this._updateRtlPosition()
  184. },
  185. _updateRtlPosition: function() {
  186. var _this = this;
  187. this._updateBounds();
  188. if (this.option("rtlEnabled") && this.option("direction") !== VERTICAL) {
  189. _common2.default.deferUpdate(function() {
  190. var containerElement = _this._container().get(0);
  191. var maxLeftOffset = containerElement.scrollWidth - containerElement.clientWidth;
  192. _common2.default.deferRender(function() {
  193. _this.scrollTo({
  194. left: maxLeftOffset
  195. })
  196. })
  197. })
  198. }
  199. },
  200. _updateBounds: function() {
  201. this._strategy.updateBounds()
  202. },
  203. _attachEventHandlers: function() {
  204. var strategy = this._strategy;
  205. var initEventData = {
  206. getDirection: strategy.getDirection.bind(strategy),
  207. validate: this._validate.bind(this),
  208. isNative: this.option("useNative"),
  209. scrollTarget: this._$container
  210. };
  211. _events_engine2.default.off(this._$wrapper, "." + SCROLLABLE);
  212. _events_engine2.default.on(this._$wrapper, _utils2.default.addNamespace(_uiEventsEmitterGesture2.default.init, SCROLLABLE), initEventData, this._initHandler.bind(this));
  213. _events_engine2.default.on(this._$wrapper, _utils2.default.addNamespace(_uiEventsEmitterGesture2.default.start, SCROLLABLE), strategy.handleStart.bind(strategy));
  214. _events_engine2.default.on(this._$wrapper, _utils2.default.addNamespace(_uiEventsEmitterGesture2.default.move, SCROLLABLE), strategy.handleMove.bind(strategy));
  215. _events_engine2.default.on(this._$wrapper, _utils2.default.addNamespace(_uiEventsEmitterGesture2.default.end, SCROLLABLE), strategy.handleEnd.bind(strategy));
  216. _events_engine2.default.on(this._$wrapper, _utils2.default.addNamespace(_uiEventsEmitterGesture2.default.cancel, SCROLLABLE), strategy.handleCancel.bind(strategy));
  217. _events_engine2.default.on(this._$wrapper, _utils2.default.addNamespace(_uiEventsEmitterGesture2.default.stop, SCROLLABLE), strategy.handleStop.bind(strategy));
  218. _events_engine2.default.off(this._$container, "." + SCROLLABLE);
  219. _events_engine2.default.on(this._$container, _utils2.default.addNamespace("scroll", SCROLLABLE), strategy.handleScroll.bind(strategy))
  220. },
  221. _validate: function(e) {
  222. if (this._isLocked()) {
  223. return false
  224. }
  225. this._updateIfNeed();
  226. return this._strategy.validate(e)
  227. },
  228. _initHandler: function() {
  229. var strategy = this._strategy;
  230. strategy.handleInit.apply(strategy, arguments)
  231. },
  232. _renderDisabledState: function() {
  233. this.$element().toggleClass(SCROLLABLE_DISABLED_CLASS, this.option("disabled"));
  234. if (this.option("disabled")) {
  235. this._lock()
  236. } else {
  237. this._unlock()
  238. }
  239. },
  240. _renderDirection: function() {
  241. this.$element().removeClass("dx-scrollable-" + HORIZONTAL).removeClass("dx-scrollable-" + VERTICAL).removeClass("dx-scrollable-" + BOTH).addClass("dx-scrollable-" + this.option("direction"))
  242. },
  243. _renderStrategy: function() {
  244. this._createStrategy();
  245. this._strategy.render();
  246. this.$element().data(SCROLLABLE_STRATEGY, this._strategy)
  247. },
  248. _createStrategy: function() {
  249. this._strategy = this.option("useNative") ? new _uiScrollable4.default(this) : new _uiScrollable2.default.SimulatedStrategy(this)
  250. },
  251. _createActions: function() {
  252. this._strategy && this._strategy.createActions()
  253. },
  254. _clean: function() {
  255. this._strategy && this._strategy.dispose()
  256. },
  257. _optionChanged: function(args) {
  258. switch (args.name) {
  259. case "onStart":
  260. case "onEnd":
  261. case "onStop":
  262. case "onUpdated":
  263. case "onScroll":
  264. case "onBounce":
  265. this._createActions();
  266. break;
  267. case "direction":
  268. this._resetInactiveDirection();
  269. this._invalidate();
  270. break;
  271. case "useNative":
  272. this._setUseSimulatedScrollbar();
  273. this._invalidate();
  274. break;
  275. case "inertiaEnabled":
  276. case "scrollByContent":
  277. case "scrollByThumb":
  278. case "bounceEnabled":
  279. case "useKeyboard":
  280. case "showScrollbar":
  281. case "useSimulatedScrollbar":
  282. case "pushBackValue":
  283. this._invalidate();
  284. break;
  285. case "disabled":
  286. this._renderDisabledState();
  287. this._strategy && this._strategy.disabledChanged();
  288. break;
  289. case "updateManually":
  290. break;
  291. case "width":
  292. this.callBase(args);
  293. this._updateRtlPosition();
  294. break;
  295. default:
  296. this.callBase(args)
  297. }
  298. },
  299. _resetInactiveDirection: function() {
  300. var inactiveProp = this._getInactiveProp();
  301. if (!inactiveProp || !_window2.default.hasWindow()) {
  302. return
  303. }
  304. var scrollOffset = this.scrollOffset();
  305. scrollOffset[inactiveProp] = 0;
  306. this.scrollTo(scrollOffset)
  307. },
  308. _getInactiveProp: function() {
  309. var direction = this.option("direction");
  310. if (direction === VERTICAL) {
  311. return "left"
  312. }
  313. if (direction === HORIZONTAL) {
  314. return "top"
  315. }
  316. },
  317. _location: function() {
  318. return this._strategy.location()
  319. },
  320. _normalizeLocation: function(location) {
  321. if (_type2.default.isPlainObject(location)) {
  322. var left = _common2.default.ensureDefined(location.left, location.x);
  323. var top = _common2.default.ensureDefined(location.top, location.y);
  324. return {
  325. left: _type2.default.isDefined(left) ? -left : void 0,
  326. top: _type2.default.isDefined(top) ? -top : void 0
  327. }
  328. } else {
  329. var direction = this.option("direction");
  330. return {
  331. left: direction !== VERTICAL ? -location : void 0,
  332. top: direction !== HORIZONTAL ? -location : void 0
  333. }
  334. }
  335. },
  336. _isLocked: function() {
  337. return this._locked
  338. },
  339. _lock: function() {
  340. this._locked = true
  341. },
  342. _unlock: function() {
  343. if (!this.option("disabled")) {
  344. this._locked = false
  345. }
  346. },
  347. _isDirection: function(direction) {
  348. var current = this.option("direction");
  349. if (direction === VERTICAL) {
  350. return current !== HORIZONTAL
  351. }
  352. if (direction === HORIZONTAL) {
  353. return current !== VERTICAL
  354. }
  355. return current === direction
  356. },
  357. _updateAllowedDirection: function() {
  358. var allowedDirections = this._strategy._allowedDirections();
  359. if (this._isDirection(BOTH) && allowedDirections.vertical && allowedDirections.horizontal) {
  360. this._allowedDirectionValue = BOTH
  361. } else {
  362. if (this._isDirection(HORIZONTAL) && allowedDirections.horizontal) {
  363. this._allowedDirectionValue = HORIZONTAL
  364. } else {
  365. if (this._isDirection(VERTICAL) && allowedDirections.vertical) {
  366. this._allowedDirectionValue = VERTICAL
  367. } else {
  368. this._allowedDirectionValue = null
  369. }
  370. }
  371. }
  372. },
  373. _allowedDirection: function() {
  374. return this._allowedDirectionValue
  375. },
  376. _container: function() {
  377. return this._$container
  378. },
  379. $content: function() {
  380. return this._$content
  381. },
  382. content: function() {
  383. return (0, _dom.getPublicElement)(this._$content)
  384. },
  385. scrollOffset: function() {
  386. var location = this._location();
  387. return {
  388. top: -location.top,
  389. left: -location.left
  390. }
  391. },
  392. scrollTop: function() {
  393. return this.scrollOffset().top
  394. },
  395. scrollLeft: function() {
  396. return this.scrollOffset().left
  397. },
  398. clientHeight: function() {
  399. return this._$container.height()
  400. },
  401. scrollHeight: function() {
  402. return this.$content().outerHeight() - 2 * this._strategy.verticalOffset()
  403. },
  404. clientWidth: function() {
  405. return this._$container.width()
  406. },
  407. scrollWidth: function() {
  408. return this.$content().outerWidth()
  409. },
  410. update: function() {
  411. if (!this._strategy) {
  412. return
  413. }
  414. return (0, _deferred.when)(this._strategy.update()).done(function() {
  415. this._updateAllowedDirection()
  416. }.bind(this))
  417. },
  418. scrollBy: function(distance) {
  419. distance = this._normalizeLocation(distance);
  420. if (!distance.top && !distance.left) {
  421. return
  422. }
  423. this._updateIfNeed();
  424. this._strategy.scrollBy(distance)
  425. },
  426. scrollTo: function(targetLocation) {
  427. targetLocation = this._normalizeLocation(targetLocation);
  428. this._updateIfNeed();
  429. var location = this._location();
  430. if (!this.option("useNative")) {
  431. targetLocation = this._strategy._applyScaleRatio(targetLocation);
  432. location = this._strategy._applyScaleRatio(location)
  433. }
  434. var distance = this._normalizeLocation({
  435. left: location.left - _common2.default.ensureDefined(targetLocation.left, location.left),
  436. top: location.top - _common2.default.ensureDefined(targetLocation.top, location.top)
  437. });
  438. if (!distance.top && !distance.left) {
  439. return
  440. }
  441. this._strategy.scrollBy(distance)
  442. },
  443. scrollToElement: function(element, offset) {
  444. offset = offset || {};
  445. var $element = (0, _renderer2.default)(element);
  446. var elementInsideContent = this.$content().find(element).length;
  447. var elementIsInsideContent = $element.parents("." + SCROLLABLE_CLASS).length - $element.parents("." + SCROLLABLE_CONTENT_CLASS).length === 0;
  448. if (!elementInsideContent || !elementIsInsideContent) {
  449. return
  450. }
  451. var scrollPosition = {
  452. top: 0,
  453. left: 0
  454. };
  455. var direction = this.option("direction");
  456. if (direction !== VERTICAL) {
  457. scrollPosition.left = this._scrollToElementPosition($element, HORIZONTAL, offset)
  458. }
  459. if (direction !== HORIZONTAL) {
  460. scrollPosition.top = this._scrollToElementPosition($element, VERTICAL, offset)
  461. }
  462. this.scrollTo(scrollPosition)
  463. },
  464. _scrollToElementPosition: function($element, direction, offset) {
  465. var isVertical = direction === VERTICAL;
  466. var startOffset = (isVertical ? offset.top : offset.left) || 0;
  467. var endOffset = (isVertical ? offset.bottom : offset.right) || 0;
  468. var pushBackOffset = isVertical ? this._strategy.verticalOffset() : 0;
  469. var elementPositionRelativeToContent = this._elementPositionRelativeToContent($element, isVertical ? "top" : "left");
  470. var elementPosition = elementPositionRelativeToContent - pushBackOffset;
  471. var elementSize = $element[isVertical ? "outerHeight" : "outerWidth"]();
  472. var scrollLocation = isVertical ? this.scrollTop() : this.scrollLeft();
  473. var clientSize = isVertical ? this.clientHeight() : this.clientWidth();
  474. var startDistance = scrollLocation - elementPosition + startOffset;
  475. var endDistance = scrollLocation - elementPosition - elementSize + clientSize - endOffset;
  476. if (startDistance <= 0 && endDistance >= 0) {
  477. return scrollLocation
  478. }
  479. return scrollLocation - (Math.abs(startDistance) > Math.abs(endDistance) ? endDistance : startDistance)
  480. },
  481. _elementPositionRelativeToContent: function($element, prop) {
  482. var result = 0;
  483. while (this._hasScrollContent($element)) {
  484. result += $element.position()[prop];
  485. $element = $element.offsetParent()
  486. }
  487. return result
  488. },
  489. _hasScrollContent: function($element) {
  490. var $content = this.$content();
  491. return $element.closest($content).length && !$element.is($content)
  492. },
  493. _updateIfNeed: function() {
  494. if (!this.option("updateManually")) {
  495. this.update()
  496. }
  497. }
  498. });
  499. (0, _component_registrator2.default)(SCROLLABLE, Scrollable);
  500. module.exports = Scrollable;
  501. module.exports.deviceDependentOptions = deviceDependentOptions;