ImagePreview.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import { use, range } from '../utils';
  2. import { preventDefault } from '../utils/event';
  3. import { PopupMixin } from '../mixins/popup';
  4. import { TouchMixin } from '../mixins/touch';
  5. import Swipe from '../swipe';
  6. import SwipeItem from '../swipe-item';
  7. const [sfc, bem] = use('image-preview');
  8. function getDistance(touches) {
  9. return Math.sqrt(
  10. Math.abs(
  11. (touches[0].clientX - touches[1].clientX) *
  12. (touches[0].clientY - touches[1].clientY)
  13. )
  14. );
  15. }
  16. export default sfc({
  17. mixins: [PopupMixin, TouchMixin],
  18. props: {
  19. images: Array,
  20. className: null,
  21. lazyLoad: Boolean,
  22. asyncClose: Boolean,
  23. startPosition: Number,
  24. showIndicators: Boolean,
  25. loop: {
  26. type: Boolean,
  27. default: true
  28. },
  29. overlay: {
  30. type: Boolean,
  31. default: true
  32. },
  33. showIndex: {
  34. type: Boolean,
  35. default: true
  36. },
  37. minZoom: {
  38. type: Number,
  39. default: 1 / 3
  40. },
  41. maxZoom: {
  42. type: Number,
  43. default: 3
  44. },
  45. overlayClass: {
  46. type: String,
  47. default: 'van-image-preview__overlay'
  48. },
  49. closeOnClickOverlay: {
  50. type: Boolean,
  51. default: true
  52. }
  53. },
  54. data() {
  55. return {
  56. scale: 1,
  57. moveX: 0,
  58. moveY: 0,
  59. moving: false,
  60. zooming: false,
  61. active: 0
  62. };
  63. },
  64. computed: {
  65. imageStyle() {
  66. const { scale } = this;
  67. const style = {
  68. transition: this.zooming || this.moving ? '' : '.3s all'
  69. };
  70. if (scale !== 1) {
  71. style.transform = `scale3d(${scale}, ${scale}, 1) translate(${this.moveX /
  72. scale}px, ${this.moveY / scale}px)`;
  73. }
  74. return style;
  75. }
  76. },
  77. watch: {
  78. value() {
  79. this.active = this.startPosition;
  80. },
  81. startPosition(active) {
  82. this.active = active;
  83. }
  84. },
  85. methods: {
  86. onWrapperTouchStart() {
  87. this.touchStartTime = new Date();
  88. },
  89. onWrapperTouchEnd(event) {
  90. preventDefault(event);
  91. const deltaTime = new Date() - this.touchStartTime;
  92. const { offsetX = 0, offsetY = 0 } = this.$refs.swipe || {};
  93. // prevent long tap to close component
  94. if (deltaTime < 300 && offsetX < 10 && offsetY < 10) {
  95. const index = this.active;
  96. this.resetScale();
  97. this.$emit('close', {
  98. index,
  99. url: this.images[index]
  100. });
  101. if (!this.asyncClose) {
  102. this.$emit('input', false);
  103. }
  104. }
  105. },
  106. startMove(event) {
  107. const image = event.currentTarget;
  108. const rect = image.getBoundingClientRect();
  109. const winWidth = window.innerWidth;
  110. const winHeight = window.innerHeight;
  111. this.touchStart(event);
  112. this.moving = true;
  113. this.startMoveX = this.moveX;
  114. this.startMoveY = this.moveY;
  115. this.maxMoveX = Math.max(0, (rect.width - winWidth) / 2);
  116. this.maxMoveY = Math.max(0, (rect.height - winHeight) / 2);
  117. },
  118. startZoom(event) {
  119. this.moving = false;
  120. this.zooming = true;
  121. this.startScale = this.scale;
  122. this.startDistance = getDistance(event.touches);
  123. },
  124. onTouchStart(event) {
  125. const { touches } = event;
  126. const { offsetX = 0 } = this.$refs.swipe || {};
  127. if (touches.length === 1 && this.scale !== 1) {
  128. this.startMove(event);
  129. } /* istanbul ignore else */ else if (touches.length === 2 && !offsetX) {
  130. this.startZoom(event);
  131. }
  132. },
  133. onTouchMove(event) {
  134. const { touches } = event;
  135. if (this.moving || this.zooming) {
  136. preventDefault(event, true);
  137. }
  138. if (this.moving) {
  139. this.touchMove(event);
  140. const moveX = this.deltaX + this.startMoveX;
  141. const moveY = this.deltaY + this.startMoveY;
  142. this.moveX = range(moveX, -this.maxMoveX, this.maxMoveX);
  143. this.moveY = range(moveY, -this.maxMoveY, this.maxMoveY);
  144. }
  145. if (this.zooming && touches.length === 2) {
  146. const distance = getDistance(touches);
  147. const scale = (this.startScale * distance) / this.startDistance;
  148. this.scale = range(scale, this.minZoom, this.maxZoom);
  149. }
  150. },
  151. onTouchEnd(event) {
  152. /* istanbul ignore else */
  153. if (this.moving || this.zooming) {
  154. let stopPropagation = true;
  155. if (
  156. this.moving &&
  157. this.startMoveX === this.moveX &&
  158. this.startMoveY === this.moveY
  159. ) {
  160. stopPropagation = false;
  161. }
  162. if (!event.touches.length) {
  163. this.moving = false;
  164. this.zooming = false;
  165. this.startMoveX = 0;
  166. this.startMoveY = 0;
  167. this.startScale = 1;
  168. if (this.scale < 1) {
  169. this.resetScale();
  170. }
  171. }
  172. if (stopPropagation) {
  173. preventDefault(event, true);
  174. }
  175. }
  176. },
  177. onChange(active) {
  178. this.resetScale();
  179. this.active = active;
  180. this.$emit('change', active);
  181. },
  182. resetScale() {
  183. this.scale = 1;
  184. this.moveX = 0;
  185. this.moveY = 0;
  186. }
  187. },
  188. render(h) {
  189. if (!this.value) {
  190. return;
  191. }
  192. const { active, images } = this;
  193. const Index = this.showIndex && (
  194. <div class={bem('index')}>
  195. {this.slots('index') || `${active + 1}/${images.length}`}
  196. </div>
  197. );
  198. const Images = (
  199. <Swipe
  200. ref="swipe"
  201. loop={this.loop}
  202. indicatorColor="white"
  203. initialSwipe={this.startPosition}
  204. showIndicators={this.showIndicators}
  205. onChange={this.onChange}
  206. >
  207. {images.map((image, index) => {
  208. const props = {
  209. class: bem('image'),
  210. style: index === active ? this.imageStyle : null,
  211. on: {
  212. touchstart: this.onTouchStart,
  213. touchmove: this.onTouchMove,
  214. touchend: this.onTouchEnd,
  215. touchcancel: this.onTouchEnd
  216. }
  217. };
  218. return (
  219. <SwipeItem>
  220. {this.lazyLoad ? (
  221. <img vLazy={image} {...props} />
  222. ) : (
  223. <img src={image} {...props} />
  224. )}
  225. </SwipeItem>
  226. );
  227. })}
  228. </Swipe>
  229. );
  230. return (
  231. <transition name="van-fade">
  232. <div
  233. class={[bem(), this.className]}
  234. onTouchstart={this.onWrapperTouchStart}
  235. onTouchend={this.onWrapperTouchEnd}
  236. onTouchcancel={this.onWrapperTouchEnd}
  237. >
  238. {Index}
  239. {Images}
  240. </div>
  241. </transition>
  242. );
  243. }
  244. });