zoom.js 22 KB

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