No Description

ons-pull-hook.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import _setImmediate from 'babel-runtime/core-js/set-immediate';
  2. import _Object$getPrototypeOf from 'babel-runtime/core-js/object/get-prototype-of';
  3. import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
  4. import _createClass from 'babel-runtime/helpers/createClass';
  5. import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
  6. import _inherits from 'babel-runtime/helpers/inherits';
  7. /*
  8. Copyright 2013-2015 ASIAL CORPORATION
  9. Licensed under the Apache License, Version 2.0 (the "License");
  10. you may not use this file except in compliance with the License.
  11. You may obtain a copy of the License at
  12. http://www.apache.org/licenses/LICENSE-2.0
  13. Unless required by applicable law or agreed to in writing, software
  14. distributed under the License is distributed on an "AS IS" BASIS,
  15. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. See the License for the specific language governing permissions and
  17. limitations under the License.
  18. */
  19. import onsElements from '../ons/elements';
  20. import util from '../ons/util';
  21. import styler from '../ons/styler';
  22. import platform from '../ons/platform';
  23. import BaseElement from './base/base-element';
  24. import GestureDetector from '../ons/gesture-detector';
  25. import animit from '../ons/animit';
  26. var STATE_INITIAL = 'initial';
  27. var STATE_PREACTION = 'preaction';
  28. var STATE_ACTION = 'action';
  29. var throwType = function throwType(el, type) {
  30. return util.throw('"' + el + '" must be ' + type);
  31. };
  32. /**
  33. * @element ons-pull-hook
  34. * @category control
  35. * @description
  36. * [en]
  37. * Component that adds **Pull to refresh** functionality to an `<ons-page>` element.
  38. *
  39. * It can be used to perform a task when the user pulls down at the top of the page. A common usage is to refresh the data displayed in a page.
  40. * [/en]
  41. * [ja][/ja]
  42. * @codepen WbJogM
  43. * @tutorial vanilla/Reference/pull-hook
  44. * @example
  45. * <ons-page>
  46. * <ons-pull-hook>
  47. * Release to refresh
  48. * </ons-pull-hook>
  49. * </ons-page>
  50. *
  51. * <script>
  52. * document.querySelector('ons-pull-hook').onAction = function(done) {
  53. * setTimeout(done, 1000);
  54. * };
  55. * </script>
  56. */
  57. var PullHookElement = function (_BaseElement) {
  58. _inherits(PullHookElement, _BaseElement);
  59. /**
  60. * @event changestate
  61. * @description
  62. * [en]Fired when the state is changed. The state can be either "initial", "preaction" or "action".[/en]
  63. * [ja]コンポーネントの状態が変わった場合に発火します。状態は、"initial", "preaction", "action"のいずれかです。[/ja]
  64. * @param {Object} event
  65. * [en]Event object.[/en]
  66. * [ja]イベントオブジェクト。[/ja]
  67. * @param {Object} event.pullHook
  68. * [en]Component object.[/en]
  69. * [ja]コンポーネントのオブジェクト。[/ja]
  70. * @param {String} event.state
  71. * [en]Current state.[/en]
  72. * [ja]現在の状態名を参照できます。[/ja]
  73. */
  74. /**
  75. * @attribute disabled
  76. * @description
  77. * [en]If this attribute is set the "pull-to-refresh" functionality is disabled.[/en]
  78. * [ja]この属性がある時、disabled状態になりアクションが実行されなくなります[/ja]
  79. */
  80. /**
  81. * @attribute height
  82. * @type {String}
  83. * @description
  84. * [en]Specify the height of the component. When pulled down further than this value it will switch to the "preaction" state. The default value is "64px".[/en]
  85. * [ja]コンポーネントの高さを指定します。この高さ以上にpull downすると"preaction"状態に移行します。デフォルトの値は"64px"です。[/ja]
  86. */
  87. /**
  88. * @attribute threshold-height
  89. * @type {String}
  90. * @description
  91. * [en]Specify the threshold height. The component automatically switches to the "action" state when pulled further than this value. The default value is "96px". A negative value will disable this property. If this value is lower than the height, it will skip "preaction" state.[/en]
  92. * [ja]閾値となる高さを指定します。この値で指定した高さよりもpull downすると、このコンポーネントは自動的に"action"状態に移行します。[/ja]
  93. */
  94. /**
  95. * @attribute fixed-content
  96. * @description
  97. * [en]If this attribute is set the content of the page will not move when pulling.[/en]
  98. * [ja]この属性がある時、プルフックが引き出されている時にもコンテンツは動きません。[/ja]
  99. */
  100. function PullHookElement() {
  101. _classCallCheck(this, PullHookElement);
  102. var _this = _possibleConstructorReturn(this, (PullHookElement.__proto__ || _Object$getPrototypeOf(PullHookElement)).call(this));
  103. _this._shouldFixScroll = util.globals.isUIWebView;
  104. _this._onDrag = _this._onDrag.bind(_this);
  105. _this._onDragStart = _this._onDragStart.bind(_this);
  106. _this._onDragEnd = _this._onDragEnd.bind(_this);
  107. _this._onScroll = _this._onScroll.bind(_this);
  108. _this._setState(STATE_INITIAL, true);
  109. _this._hide(); // Fix for transparent toolbar transitions
  110. return _this;
  111. }
  112. _createClass(PullHookElement, [{
  113. key: '_setStyle',
  114. value: function _setStyle() {
  115. var height = this.height + 'px';
  116. styler(this, { height: height, lineHeight: height });
  117. this.style.display === '' && this._show();
  118. }
  119. }, {
  120. key: '_onScroll',
  121. value: function _onScroll(event) {
  122. var element = this._pageElement;
  123. if (element.scrollTop < 0) {
  124. element.scrollTop = 0;
  125. }
  126. }
  127. }, {
  128. key: '_canConsumeGesture',
  129. value: function _canConsumeGesture(gesture) {
  130. return gesture.direction === 'up' || gesture.direction === 'down';
  131. }
  132. }, {
  133. key: '_onDragStart',
  134. value: function _onDragStart(event) {
  135. var _this2 = this;
  136. if (!event.gesture || this.disabled) {
  137. return;
  138. }
  139. var tapY = event.gesture.center.clientY + this._pageElement.scrollTop;
  140. var maxY = window.innerHeight;
  141. // Only use drags that start near the pullHook to reduce flickerings
  142. var draggableAreaRatio = this._shouldFixScroll ? .8 : 1;
  143. this._ignoreDrag = event.consumed || tapY > maxY * draggableAreaRatio;
  144. if (!this._ignoreDrag) {
  145. var consume = event.consume;
  146. event.consume = function () {
  147. consume && consume();
  148. _this2._ignoreDrag = true;
  149. // This elements resizes .page__content so it is safer
  150. // to hide it when other components are dragged.
  151. _this2._hide();
  152. };
  153. if (this._canConsumeGesture(event.gesture)) {
  154. consume && consume();
  155. event.consumed = true;
  156. this._show(); // Not enough due to 'dragLockAxis'
  157. }
  158. }
  159. this._startScroll = this._pageElement.scrollTop;
  160. }
  161. }, {
  162. key: '_onDrag',
  163. value: function _onDrag(event) {
  164. var _this3 = this;
  165. if (!event.gesture || this.disabled || this._ignoreDrag || !this._canConsumeGesture(event.gesture)) {
  166. return;
  167. }
  168. // Necessary due to 'dragLockAxis' (25px)
  169. if (this.style.display === 'none') {
  170. this._show();
  171. }
  172. event.stopPropagation();
  173. var tapY = event.gesture.center.clientY + this._pageElement.scrollTop;
  174. var maxY = window.innerHeight;
  175. // Hack to make it work on Android 4.4 WebView and iOS UIWebView. Scrolls manually
  176. // near the top of the page so there will be no inertial scroll when scrolling down.
  177. // Allowing default scrolling will kill all 'touchmove' events.
  178. if (this._shouldFixScroll) {
  179. this._pageElement.scrollTop = this._startScroll - event.gesture.deltaY;
  180. // Allow inertia when scrolling down below 50% of the view to reduce flickerings
  181. if (event.gesture.interimDirection !== 'up' || tapY <= maxY * .5) {
  182. event.gesture.preventDefault();
  183. }
  184. }
  185. var scroll = Math.max(event.gesture.deltaY - this._startScroll, 0);
  186. if (scroll !== this._currentTranslation) {
  187. var th = this.thresholdHeight;
  188. if (th > 0 && scroll >= th) {
  189. event.gesture.stopDetect();
  190. _setImmediate(function () {
  191. return _this3._finish();
  192. });
  193. } else if (scroll >= this.height) {
  194. this._setState(STATE_PREACTION);
  195. } else {
  196. this._setState(STATE_INITIAL);
  197. }
  198. this._translateTo(scroll);
  199. }
  200. }
  201. }, {
  202. key: '_onDragEnd',
  203. value: function _onDragEnd(event) {
  204. if (!event.gesture || this.disabled || this._ignoreDrag) {
  205. return;
  206. }
  207. event.stopPropagation();
  208. if (this._currentTranslation > 0) {
  209. var scroll = this._currentTranslation;
  210. if (scroll > this.height) {
  211. this._finish();
  212. } else {
  213. this._translateTo(0, { animate: true });
  214. }
  215. }
  216. }
  217. /**
  218. * @property onAction
  219. * @type {Function}
  220. * @description
  221. * [en]This will be called in the `action` state if it exists. The function will be given a `done` callback as it's first argument.[/en]
  222. * [ja][/ja]
  223. */
  224. }, {
  225. key: '_finish',
  226. value: function _finish() {
  227. var _this4 = this;
  228. this._setState(STATE_ACTION);
  229. this._translateTo(this.height, { animate: true });
  230. var action = this.onAction || function (done) {
  231. return done();
  232. };
  233. action(function () {
  234. _this4._translateTo(0, { animate: true });
  235. _this4._setState(STATE_INITIAL);
  236. });
  237. }
  238. /**
  239. * @property height
  240. * @type {Number}
  241. * @description
  242. * [en]The height of the pull hook in pixels. The default value is `64px`.[/en]
  243. * [ja][/ja]
  244. */
  245. }, {
  246. key: '_setState',
  247. value: function _setState(state, noEvent) {
  248. var lastState = this.state;
  249. this.setAttribute('state', state);
  250. if (!noEvent && lastState !== this.state) {
  251. util.triggerElementEvent(this, 'changestate', {
  252. pullHook: this,
  253. state: state,
  254. lastState: lastState
  255. });
  256. }
  257. }
  258. /**
  259. * @property state
  260. * @readonly
  261. * @type {String}
  262. * @description
  263. * [en]Current state of the element.[/en]
  264. * [ja][/ja]
  265. */
  266. }, {
  267. key: '_show',
  268. value: function _show() {
  269. var _this5 = this;
  270. // Run asyncrhonously to avoid conflicts with Animit's style clean
  271. _setImmediate(function () {
  272. _this5.style.display = '';
  273. if (_this5._pageElement) {
  274. _this5._pageElement.style.marginTop = '-' + _this5.height + 'px';
  275. }
  276. });
  277. }
  278. }, {
  279. key: '_hide',
  280. value: function _hide() {
  281. this.style.display = 'none';
  282. if (this._pageElement) {
  283. this._pageElement.style.marginTop = '';
  284. }
  285. }
  286. /**
  287. * @param {Number} scroll
  288. * @param {Object} options
  289. * @param {Function} [options.callback]
  290. */
  291. }, {
  292. key: '_translateTo',
  293. value: function _translateTo(scroll) {
  294. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  295. if (this._currentTranslation == 0 && scroll == 0) {
  296. return;
  297. }
  298. this._currentTranslation = scroll;
  299. var opt = options.animate ? { duration: .3, timing: 'cubic-bezier(.1, .7, .1, 1)' } : {};
  300. this._onPull && this._onPull((scroll / this.height).toFixed(2), opt);
  301. var scrollElement = this.hasAttribute('fixed-content') ? this : this._pageElement;
  302. animit(scrollElement).queue({ transform: 'translate3d(0px, ' + scroll + 'px, 0px)' }, opt).play(function () {
  303. scroll === 0 && styler.clear(scrollElement, 'transition transform');
  304. options.callback instanceof Function && options.callback();
  305. });
  306. }
  307. }, {
  308. key: '_disableDragLock',
  309. value: function _disableDragLock() {
  310. // e2e tests need it
  311. this._dragLockDisabled = true;
  312. this._setupListeners(true);
  313. }
  314. }, {
  315. key: '_setupListeners',
  316. value: function _setupListeners(add) {
  317. var _this6 = this;
  318. var scrollToggle = function scrollToggle(action) {
  319. return _this6._pageElement[action + 'EventListener']('scroll', _this6._onScroll, false);
  320. };
  321. var gdToggle = function gdToggle(action) {
  322. var passive = { passive: true };
  323. _this6._gestureDetector[action]('drag', _this6._onDrag, passive);
  324. _this6._gestureDetector[action]('dragstart', _this6._onDragStart, passive);
  325. _this6._gestureDetector[action]('dragend', _this6._onDragEnd, passive);
  326. };
  327. if (this._gestureDetector) {
  328. gdToggle('off');
  329. this._gestureDetector.dispose();
  330. this._gestureDetector = null;
  331. }
  332. scrollToggle('remove');
  333. if (add) {
  334. this._gestureDetector = new GestureDetector(this._pageElement, {
  335. dragMinDistance: 1,
  336. dragDistanceCorrection: false,
  337. dragLockToAxis: !this._dragLockDisabled,
  338. passive: !this._shouldFixScroll
  339. });
  340. gdToggle('on');
  341. scrollToggle('add');
  342. }
  343. }
  344. }, {
  345. key: 'connectedCallback',
  346. value: function connectedCallback() {
  347. this._currentTranslation = 0;
  348. this._pageElement = this.parentNode;
  349. this._setupListeners(true);
  350. this._setStyle();
  351. }
  352. }, {
  353. key: 'disconnectedCallback',
  354. value: function disconnectedCallback() {
  355. this._hide();
  356. this._setupListeners(false);
  357. }
  358. }, {
  359. key: 'attributeChangedCallback',
  360. value: function attributeChangedCallback(name, last, current) {
  361. if (name === 'height' && this._pageElement) {
  362. this._setStyle();
  363. }
  364. }
  365. }, {
  366. key: 'onAction',
  367. get: function get() {
  368. return this._onAction;
  369. },
  370. set: function set(value) {
  371. if (value && !(value instanceof Function)) {
  372. throwType('onAction', 'function or null');
  373. }
  374. this._onAction = value;
  375. }
  376. /**
  377. * @property onPull
  378. * @type {Function}
  379. * @description
  380. * [en]Hook called whenever the user pulls the element. It gets the pulled distance ratio (scroll / height) and an animationOptions object as arguments.[/en]
  381. * [ja][/ja]
  382. */
  383. }, {
  384. key: 'onPull',
  385. get: function get() {
  386. return this._onPull;
  387. },
  388. set: function set(value) {
  389. if (value && !(value instanceof Function)) {
  390. throwType('onPull', 'function or null');
  391. }
  392. this._onPull = value;
  393. }
  394. }, {
  395. key: 'height',
  396. set: function set(value) {
  397. if (!util.isInteger(value)) {
  398. throwType('height', 'integer');
  399. }
  400. this.setAttribute('height', value + 'px');
  401. },
  402. get: function get() {
  403. return parseInt(this.getAttribute('height') || '64', 10);
  404. }
  405. /**
  406. * @property thresholdHeight
  407. * @type {Number}
  408. * @description
  409. * [en]The thresholdHeight of the pull hook in pixels. The default value is `96px`.[/en]
  410. * [ja][/ja]
  411. */
  412. }, {
  413. key: 'thresholdHeight',
  414. set: function set(value) {
  415. if (!util.isInteger(value)) {
  416. throwType('thresholdHeight', 'integer');
  417. }
  418. this.setAttribute('threshold-height', value + 'px');
  419. },
  420. get: function get() {
  421. return parseInt(this.getAttribute('threshold-height') || '96', 10);
  422. }
  423. }, {
  424. key: 'state',
  425. get: function get() {
  426. return this.getAttribute('state');
  427. }
  428. /**
  429. * @property pullDistance
  430. * @readonly
  431. * @type {Number}
  432. * @description
  433. * [en]The current number of pixels the pull hook has moved.[/en]
  434. * [ja]現在のプルフックが引き出された距離をピクセル数。[/ja]
  435. */
  436. }, {
  437. key: 'pullDistance',
  438. get: function get() {
  439. return this._currentTranslation;
  440. }
  441. /**
  442. * @property disabled
  443. * @type {Boolean}
  444. * @description
  445. * [en]Whether the element is disabled or not.[/en]
  446. * [ja]無効化されている場合に`true`。[/ja]
  447. */
  448. }, {
  449. key: 'disabled',
  450. set: function set(value) {
  451. return util.toggleAttribute(this, 'disabled', value);
  452. },
  453. get: function get() {
  454. return this.hasAttribute('disabled');
  455. }
  456. }], [{
  457. key: 'observedAttributes',
  458. get: function get() {
  459. return ['height'];
  460. }
  461. }, {
  462. key: 'events',
  463. get: function get() {
  464. return ['changestate'];
  465. }
  466. }]);
  467. return PullHookElement;
  468. }(BaseElement);
  469. export default PullHookElement;
  470. onsElements.PullHook = PullHookElement;
  471. customElements.define('ons-pull-hook', PullHookElement);