zoom.esm.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. /*!
  2. * better-scroll / zoom
  3. * (c) 2016-2021 ustbhuangyi
  4. * Released under the MIT License.
  5. */
  6. var sourcePrefix = 'plugins.zoom';
  7. var propertiesMap = [
  8. {
  9. key: 'zoomTo',
  10. name: 'zoomTo'
  11. }
  12. ];
  13. var propertiesConfig = propertiesMap.map(function (item) {
  14. return {
  15. key: item.key,
  16. sourceKey: sourcePrefix + "." + item.name
  17. };
  18. });
  19. // ssr support
  20. var inBrowser = typeof window !== 'undefined';
  21. var ua = inBrowser && navigator.userAgent.toLowerCase();
  22. !!(ua && /wechatdevtools/.test(ua));
  23. ua && ua.indexOf('android') > 0;
  24. /* istanbul ignore next */
  25. ((function () {
  26. if (typeof ua === 'string') {
  27. var regex = /os (\d\d?_\d(_\d)?)/;
  28. var matches = regex.exec(ua);
  29. if (!matches)
  30. return false;
  31. var parts = matches[1].split('_').map(function (item) {
  32. return parseInt(item, 10);
  33. });
  34. // ios version >= 13.4 issue 982
  35. return !!(parts[0] === 13 && parts[1] >= 4);
  36. }
  37. return false;
  38. }))();
  39. /* istanbul ignore next */
  40. var supportsPassive = false;
  41. /* istanbul ignore next */
  42. if (inBrowser) {
  43. var EventName = 'test-passive';
  44. try {
  45. var opts = {};
  46. Object.defineProperty(opts, 'passive', {
  47. get: function () {
  48. supportsPassive = true;
  49. },
  50. }); // https://github.com/facebook/flow/issues/285
  51. window.addEventListener(EventName, function () { }, opts);
  52. }
  53. catch (e) { }
  54. }
  55. function getNow() {
  56. return window.performance &&
  57. window.performance.now &&
  58. window.performance.timing
  59. ? window.performance.now() + window.performance.timing.navigationStart
  60. : +new Date();
  61. }
  62. var extend = function (target, source) {
  63. for (var key in source) {
  64. target[key] = source[key];
  65. }
  66. return target;
  67. };
  68. function getDistance(x, y) {
  69. return Math.sqrt(x * x + y * y);
  70. }
  71. function between(x, min, max) {
  72. if (x < min) {
  73. return min;
  74. }
  75. if (x > max) {
  76. return max;
  77. }
  78. return x;
  79. }
  80. var elementStyle = (inBrowser &&
  81. document.createElement('div').style);
  82. var vendor = (function () {
  83. /* istanbul ignore if */
  84. if (!inBrowser) {
  85. return false;
  86. }
  87. var transformNames = [
  88. {
  89. key: 'standard',
  90. value: 'transform',
  91. },
  92. {
  93. key: 'webkit',
  94. value: 'webkitTransform',
  95. },
  96. {
  97. key: 'Moz',
  98. value: 'MozTransform',
  99. },
  100. {
  101. key: 'O',
  102. value: 'OTransform',
  103. },
  104. {
  105. key: 'ms',
  106. value: 'msTransform',
  107. },
  108. ];
  109. for (var _i = 0, transformNames_1 = transformNames; _i < transformNames_1.length; _i++) {
  110. var obj = transformNames_1[_i];
  111. if (elementStyle[obj.value] !== undefined) {
  112. return obj.key;
  113. }
  114. }
  115. /* istanbul ignore next */
  116. return false;
  117. })();
  118. /* istanbul ignore next */
  119. function prefixStyle(style) {
  120. if (vendor === false) {
  121. return style;
  122. }
  123. if (vendor === 'standard') {
  124. if (style === 'transitionEnd') {
  125. return 'transitionend';
  126. }
  127. return style;
  128. }
  129. return vendor + style.charAt(0).toUpperCase() + style.substr(1);
  130. }
  131. function offsetToBody(el) {
  132. var rect = el.getBoundingClientRect();
  133. return {
  134. left: -(rect.left + window.pageXOffset),
  135. top: -(rect.top + window.pageYOffset),
  136. };
  137. }
  138. vendor && vendor !== 'standard' ? '-' + vendor.toLowerCase() + '-' : '';
  139. var transform = prefixStyle('transform');
  140. var transition = prefixStyle('transition');
  141. inBrowser && prefixStyle('perspective') in elementStyle;
  142. var style = {
  143. transform: transform,
  144. transition: transition,
  145. transitionTimingFunction: prefixStyle('transitionTimingFunction'),
  146. transitionDuration: prefixStyle('transitionDuration'),
  147. transitionDelay: prefixStyle('transitionDelay'),
  148. transformOrigin: prefixStyle('transformOrigin'),
  149. transitionEnd: prefixStyle('transitionEnd'),
  150. transitionProperty: prefixStyle('transitionProperty'),
  151. };
  152. function getRect(el) {
  153. /* istanbul ignore if */
  154. if (el instanceof window.SVGElement) {
  155. var rect = el.getBoundingClientRect();
  156. return {
  157. top: rect.top,
  158. left: rect.left,
  159. width: rect.width,
  160. height: rect.height,
  161. };
  162. }
  163. else {
  164. return {
  165. top: el.offsetTop,
  166. left: el.offsetLeft,
  167. width: el.offsetWidth,
  168. height: el.offsetHeight,
  169. };
  170. }
  171. }
  172. var ease = {
  173. // easeOutQuint
  174. swipe: {
  175. style: 'cubic-bezier(0.23, 1, 0.32, 1)',
  176. fn: function (t) {
  177. return 1 + --t * t * t * t * t;
  178. }
  179. },
  180. // easeOutQuard
  181. swipeBounce: {
  182. style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
  183. fn: function (t) {
  184. return t * (2 - t);
  185. }
  186. },
  187. // easeOutQuart
  188. bounce: {
  189. style: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
  190. fn: function (t) {
  191. return 1 - --t * t * t * t;
  192. }
  193. }
  194. };
  195. var DEFAULT_INTERVAL = 1000 / 60;
  196. var windowCompat = inBrowser && window;
  197. /* istanbul ignore next */
  198. function noop() { }
  199. var requestAnimationFrame = (function () {
  200. /* istanbul ignore if */
  201. if (!inBrowser) {
  202. return noop;
  203. }
  204. return (windowCompat.requestAnimationFrame ||
  205. windowCompat.webkitRequestAnimationFrame ||
  206. windowCompat.mozRequestAnimationFrame ||
  207. windowCompat.oRequestAnimationFrame ||
  208. // if all else fails, use setTimeout
  209. function (callback) {
  210. return window.setTimeout(callback, callback.interval || DEFAULT_INTERVAL); // make interval as precise as possible.
  211. });
  212. })();
  213. var cancelAnimationFrame = (function () {
  214. /* istanbul ignore if */
  215. if (!inBrowser) {
  216. return noop;
  217. }
  218. return (windowCompat.cancelAnimationFrame ||
  219. windowCompat.webkitCancelAnimationFrame ||
  220. windowCompat.mozCancelAnimationFrame ||
  221. windowCompat.oCancelAnimationFrame ||
  222. function (id) {
  223. window.clearTimeout(id);
  224. });
  225. })();
  226. var TWO_FINGERS = 2;
  227. var RAW_SCALE = 1;
  228. var Zoom = /** @class */ (function () {
  229. function Zoom(scroll) {
  230. this.scroll = scroll;
  231. this.scale = RAW_SCALE;
  232. this.prevScale = 1;
  233. this.init();
  234. }
  235. Zoom.prototype.init = function () {
  236. this.handleBScroll();
  237. this.handleOptions();
  238. this.handleHooks();
  239. this.tryInitialZoomTo(this.zoomOpt);
  240. };
  241. Zoom.prototype.zoomTo = function (scale, x, y, bounceTime) {
  242. var _a = this.resolveOrigin(x, y), originX = _a.originX, originY = _a.originY;
  243. var origin = {
  244. x: originX,
  245. y: originY,
  246. baseScale: this.scale,
  247. };
  248. this._doZoomTo(scale, origin, bounceTime, true);
  249. };
  250. Zoom.prototype.handleBScroll = function () {
  251. this.scroll.proxy(propertiesConfig);
  252. this.scroll.registerType([
  253. 'beforeZoomStart',
  254. 'zoomStart',
  255. 'zooming',
  256. 'zoomEnd',
  257. ]);
  258. };
  259. Zoom.prototype.handleOptions = function () {
  260. var userOptions = (this.scroll.options.zoom === true
  261. ? {}
  262. : this.scroll.options.zoom);
  263. var defaultOptions = {
  264. start: 1,
  265. min: 1,
  266. max: 4,
  267. initialOrigin: [0, 0],
  268. minimalZoomDistance: 5,
  269. bounceTime: 800,
  270. };
  271. this.zoomOpt = extend(defaultOptions, userOptions);
  272. };
  273. Zoom.prototype.handleHooks = function () {
  274. var _this = this;
  275. var scroll = this.scroll;
  276. var scroller = this.scroll.scroller;
  277. this.wrapper = this.scroll.scroller.wrapper;
  278. this.setTransformOrigin(this.scroll.scroller.content);
  279. var scrollBehaviorX = scroller.scrollBehaviorX;
  280. var scrollBehaviorY = scroller.scrollBehaviorY;
  281. this.hooksFn = [];
  282. // BScroll
  283. this.registerHooks(scroll.hooks, scroll.hooks.eventTypes.contentChanged, function (content) {
  284. _this.setTransformOrigin(content);
  285. _this.scale = RAW_SCALE;
  286. _this.tryInitialZoomTo(_this.zoomOpt);
  287. });
  288. this.registerHooks(scroll.hooks, scroll.hooks.eventTypes.beforeInitialScrollTo, function () {
  289. // if perform a zoom action, we should prevent initial scroll when initialised
  290. if (_this.zoomOpt.start !== RAW_SCALE) {
  291. return true;
  292. }
  293. });
  294. // enlarge boundary
  295. this.registerHooks(scrollBehaviorX.hooks, scrollBehaviorX.hooks.eventTypes.beforeComputeBoundary, function () {
  296. // content may change, don't cache it's size
  297. var contentSize = getRect(_this.scroll.scroller.content);
  298. scrollBehaviorX.contentSize = Math.floor(contentSize.width * _this.scale);
  299. });
  300. this.registerHooks(scrollBehaviorY.hooks, scrollBehaviorY.hooks.eventTypes.beforeComputeBoundary, function () {
  301. // content may change, don't cache it's size
  302. var contentSize = getRect(_this.scroll.scroller.content);
  303. scrollBehaviorY.contentSize = Math.floor(contentSize.height * _this.scale);
  304. });
  305. // touch event
  306. this.registerHooks(scroller.actions.hooks, scroller.actions.hooks.eventTypes.start, function (e) {
  307. var numberOfFingers = (e.touches && e.touches.length) || 0;
  308. _this.fingersOperation(numberOfFingers);
  309. if (numberOfFingers === TWO_FINGERS) {
  310. _this.zoomStart(e);
  311. }
  312. });
  313. this.registerHooks(scroller.actions.hooks, scroller.actions.hooks.eventTypes.beforeMove, function (e) {
  314. var numberOfFingers = (e.touches && e.touches.length) || 0;
  315. _this.fingersOperation(numberOfFingers);
  316. if (numberOfFingers === TWO_FINGERS) {
  317. _this.zoom(e);
  318. return true;
  319. }
  320. });
  321. this.registerHooks(scroller.actions.hooks, scroller.actions.hooks.eventTypes.beforeEnd, function (e) {
  322. var numberOfFingers = _this.fingersOperation();
  323. if (numberOfFingers === TWO_FINGERS) {
  324. _this.zoomEnd();
  325. return true;
  326. }
  327. });
  328. this.registerHooks(scroller.translater.hooks, scroller.translater.hooks.eventTypes.beforeTranslate, function (transformStyle, point) {
  329. var scale = point.scale ? point.scale : _this.prevScale;
  330. _this.prevScale = scale;
  331. transformStyle.push("scale(" + scale + ")");
  332. });
  333. this.registerHooks(scroller.hooks, scroller.hooks.eventTypes.scrollEnd, function () {
  334. if (_this.fingersOperation() === TWO_FINGERS) {
  335. _this.scroll.trigger(_this.scroll.eventTypes.zoomEnd, {
  336. scale: _this.scale,
  337. });
  338. }
  339. });
  340. this.registerHooks(this.scroll.hooks, 'destroy', this.destroy);
  341. };
  342. Zoom.prototype.setTransformOrigin = function (content) {
  343. content.style[style.transformOrigin] = '0 0';
  344. };
  345. Zoom.prototype.tryInitialZoomTo = function (options) {
  346. var start = options.start, initialOrigin = options.initialOrigin;
  347. var _a = this.scroll.scroller, scrollBehaviorX = _a.scrollBehaviorX, scrollBehaviorY = _a.scrollBehaviorY;
  348. if (start !== RAW_SCALE) {
  349. // Movable plugin may wanna modify minScrollPos or maxScrollPos
  350. // so we force Movable to caculate them
  351. this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]);
  352. this.zoomTo(start, initialOrigin[0], initialOrigin[1], 0);
  353. }
  354. };
  355. // getter or setter operation
  356. Zoom.prototype.fingersOperation = function (amounts) {
  357. if (typeof amounts === 'number') {
  358. this.numberOfFingers = amounts;
  359. }
  360. else {
  361. return this.numberOfFingers;
  362. }
  363. };
  364. Zoom.prototype._doZoomTo = function (scale, origin, time, useCurrentPos) {
  365. var _this = this;
  366. if (time === void 0) { time = this.zoomOpt.bounceTime; }
  367. if (useCurrentPos === void 0) { useCurrentPos = false; }
  368. var _a = this.zoomOpt, min = _a.min, max = _a.max;
  369. var fromScale = this.scale;
  370. var toScale = between(scale, min, max);
  371. (function () {
  372. if (time === 0) {
  373. _this.scroll.trigger(_this.scroll.eventTypes.zooming, {
  374. scale: toScale,
  375. });
  376. return;
  377. }
  378. if (time > 0) {
  379. var timer_1;
  380. var startTime_1 = getNow();
  381. var endTime_1 = startTime_1 + time;
  382. var scheduler_1 = function () {
  383. var now = getNow();
  384. if (now >= endTime_1) {
  385. _this.scroll.trigger(_this.scroll.eventTypes.zooming, {
  386. scale: toScale,
  387. });
  388. cancelAnimationFrame(timer_1);
  389. return;
  390. }
  391. var ratio = ease.bounce.fn((now - startTime_1) / time);
  392. var currentScale = ratio * (toScale - fromScale) + fromScale;
  393. _this.scroll.trigger(_this.scroll.eventTypes.zooming, {
  394. scale: currentScale,
  395. });
  396. timer_1 = requestAnimationFrame(scheduler_1);
  397. };
  398. // start scheduler job
  399. scheduler_1();
  400. }
  401. })();
  402. // suppose you are zooming by two fingers
  403. this.fingersOperation(2);
  404. this._zoomTo(toScale, fromScale, origin, time, useCurrentPos);
  405. };
  406. Zoom.prototype._zoomTo = function (toScale, fromScale, origin, time, useCurrentPos) {
  407. if (useCurrentPos === void 0) { useCurrentPos = false; }
  408. var ratio = toScale / origin.baseScale;
  409. this.setScale(toScale);
  410. var scroller = this.scroll.scroller;
  411. var scrollBehaviorX = scroller.scrollBehaviorX, scrollBehaviorY = scroller.scrollBehaviorY;
  412. this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]);
  413. // position is restrained in boundary
  414. var newX = this.getNewPos(origin.x, ratio, scrollBehaviorX, true, useCurrentPos);
  415. var newY = this.getNewPos(origin.y, ratio, scrollBehaviorY, true, useCurrentPos);
  416. if (scrollBehaviorX.currentPos !== Math.round(newX) ||
  417. scrollBehaviorY.currentPos !== Math.round(newY) ||
  418. toScale !== fromScale) {
  419. scroller.scrollTo(newX, newY, time, ease.bounce, {
  420. start: {
  421. scale: fromScale,
  422. },
  423. end: {
  424. scale: toScale,
  425. },
  426. });
  427. }
  428. };
  429. Zoom.prototype.resolveOrigin = function (x, y) {
  430. var _a = this.scroll.scroller, scrollBehaviorX = _a.scrollBehaviorX, scrollBehaviorY = _a.scrollBehaviorY;
  431. var resolveFormula = {
  432. left: function () {
  433. return 0;
  434. },
  435. top: function () {
  436. return 0;
  437. },
  438. right: function () {
  439. return scrollBehaviorX.contentSize;
  440. },
  441. bottom: function () {
  442. return scrollBehaviorY.contentSize;
  443. },
  444. center: function (index) {
  445. var baseSize = index === 0
  446. ? scrollBehaviorX.contentSize
  447. : scrollBehaviorY.contentSize;
  448. return baseSize / 2;
  449. },
  450. };
  451. return {
  452. originX: typeof x === 'number' ? x : resolveFormula[x](0),
  453. originY: typeof y === 'number' ? y : resolveFormula[y](1),
  454. };
  455. };
  456. Zoom.prototype.zoomStart = function (e) {
  457. var firstFinger = e.touches[0];
  458. var secondFinger = e.touches[1];
  459. this.startDistance = this.getFingerDistance(e);
  460. this.startScale = this.scale;
  461. var _a = offsetToBody(this.wrapper), left = _a.left, top = _a.top;
  462. this.origin = {
  463. x: Math.abs(firstFinger.pageX + secondFinger.pageX) / 2 +
  464. left -
  465. this.scroll.x,
  466. y: Math.abs(firstFinger.pageY + secondFinger.pageY) / 2 +
  467. top -
  468. this.scroll.y,
  469. baseScale: this.startScale,
  470. };
  471. this.scroll.trigger(this.scroll.eventTypes.beforeZoomStart);
  472. };
  473. Zoom.prototype.zoom = function (e) {
  474. var currentDistance = this.getFingerDistance(e);
  475. // at least minimalZoomDistance pixels for the zoom to initiate
  476. if (!this.zoomed &&
  477. Math.abs(currentDistance - this.startDistance) <
  478. this.zoomOpt.minimalZoomDistance) {
  479. return;
  480. }
  481. // when out of boundary , perform a damping algorithm
  482. var endScale = this.dampingScale((currentDistance / this.startDistance) * this.startScale);
  483. var ratio = endScale / this.startScale;
  484. this.setScale(endScale);
  485. if (!this.zoomed) {
  486. this.zoomed = true;
  487. this.scroll.trigger(this.scroll.eventTypes.zoomStart);
  488. }
  489. var scroller = this.scroll.scroller;
  490. var scrollBehaviorX = scroller.scrollBehaviorX, scrollBehaviorY = scroller.scrollBehaviorY;
  491. var x = this.getNewPos(this.origin.x, ratio, scrollBehaviorX, false, false);
  492. var y = this.getNewPos(this.origin.y, ratio, scrollBehaviorY, false, false);
  493. this.scroll.trigger(this.scroll.eventTypes.zooming, {
  494. scale: this.scale,
  495. });
  496. scroller.translater.translate({ x: x, y: y, scale: endScale });
  497. };
  498. Zoom.prototype.zoomEnd = function () {
  499. if (!this.zoomed)
  500. return;
  501. // if out of boundary, do rebound!
  502. if (this.shouldRebound()) {
  503. this._doZoomTo(this.scale, this.origin, this.zoomOpt.bounceTime);
  504. return;
  505. }
  506. this.scroll.trigger(this.scroll.eventTypes.zoomEnd, { scale: this.scale });
  507. };
  508. Zoom.prototype.getFingerDistance = function (e) {
  509. var firstFinger = e.touches[0];
  510. var secondFinger = e.touches[1];
  511. var deltaX = Math.abs(firstFinger.pageX - secondFinger.pageX);
  512. var deltaY = Math.abs(firstFinger.pageY - secondFinger.pageY);
  513. return getDistance(deltaX, deltaY);
  514. };
  515. Zoom.prototype.shouldRebound = function () {
  516. var _a = this.zoomOpt, min = _a.min, max = _a.max;
  517. var currentScale = this.scale;
  518. // scale exceeded!
  519. if (currentScale !== between(currentScale, min, max)) {
  520. return true;
  521. }
  522. var _b = this.scroll.scroller, scrollBehaviorX = _b.scrollBehaviorX, scrollBehaviorY = _b.scrollBehaviorY;
  523. // enlarge boundaries manually when zoom is end
  524. this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]);
  525. var xInBoundary = scrollBehaviorX.checkInBoundary().inBoundary;
  526. var yInBoundary = scrollBehaviorX.checkInBoundary().inBoundary;
  527. return !(xInBoundary && yInBoundary);
  528. };
  529. Zoom.prototype.dampingScale = function (scale) {
  530. var _a = this.zoomOpt, min = _a.min, max = _a.max;
  531. if (scale < min) {
  532. scale = 0.5 * min * Math.pow(2.0, scale / min);
  533. }
  534. else if (scale > max) {
  535. scale = 2.0 * max * Math.pow(0.5, max / scale);
  536. }
  537. return scale;
  538. };
  539. Zoom.prototype.setScale = function (scale) {
  540. this.scale = scale;
  541. };
  542. Zoom.prototype.resetBoundaries = function (scrollBehaviorPairs) {
  543. scrollBehaviorPairs.forEach(function (behavior) { return behavior.computeBoundary(); });
  544. };
  545. Zoom.prototype.getNewPos = function (origin, lastScale, scrollBehavior, shouldInBoundary, useCurrentPos) {
  546. if (useCurrentPos === void 0) { useCurrentPos = false; }
  547. var newPos = origin -
  548. origin * lastScale +
  549. (useCurrentPos ? scrollBehavior.currentPos : scrollBehavior.startPos);
  550. if (shouldInBoundary) {
  551. newPos = between(newPos, scrollBehavior.maxScrollPos, scrollBehavior.minScrollPos);
  552. }
  553. // maxScrollPos or minScrollPos maybe a negative or positive digital
  554. return newPos > 0 ? Math.floor(newPos) : Math.ceil(newPos);
  555. };
  556. Zoom.prototype.registerHooks = function (hooks, name, handler) {
  557. hooks.on(name, handler, this);
  558. this.hooksFn.push([hooks, name, handler]);
  559. };
  560. Zoom.prototype.destroy = function () {
  561. this.hooksFn.forEach(function (item) {
  562. var hooks = item[0];
  563. var hooksName = item[1];
  564. var handlerFn = item[2];
  565. hooks.off(hooksName, handlerFn);
  566. });
  567. this.hooksFn.length = 0;
  568. };
  569. Zoom.pluginName = 'zoom';
  570. return Zoom;
  571. }());
  572. export default Zoom;