FloatingBubble.mjs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { Teleport, computed, defineComponent, nextTick, onMounted, ref, watch, onActivated, onDeactivated, createVNode as _createVNode, vShow as _vShow, mergeProps as _mergeProps, withDirectives as _withDirectives } from "vue";
  2. import { pick, addUnit, closest, createNamespace, makeNumberProp, makeStringProp, windowWidth, windowHeight } from "../utils/index.mjs";
  3. import { useRect, useEventListener } from "@vant/use";
  4. import { useTouch } from "../composables/use-touch.mjs";
  5. import Icon from "../icon/index.mjs";
  6. const floatingBubbleProps = {
  7. gap: makeNumberProp(24),
  8. icon: String,
  9. axis: makeStringProp("y"),
  10. magnetic: String,
  11. offset: {
  12. type: Object,
  13. default: () => ({
  14. x: -1,
  15. y: -1
  16. })
  17. },
  18. teleport: {
  19. type: [String, Object],
  20. default: "body"
  21. }
  22. };
  23. const [name, bem] = createNamespace("floating-bubble");
  24. var stdin_default = defineComponent({
  25. name,
  26. inheritAttrs: false,
  27. props: floatingBubbleProps,
  28. emits: ["click", "update:offset", "offsetChange"],
  29. setup(props, {
  30. slots,
  31. emit,
  32. attrs
  33. }) {
  34. const rootRef = ref();
  35. const state = ref({
  36. x: 0,
  37. y: 0,
  38. width: 0,
  39. height: 0
  40. });
  41. const boundary = computed(() => ({
  42. top: props.gap,
  43. right: windowWidth.value - state.value.width - props.gap,
  44. bottom: windowHeight.value - state.value.height - props.gap,
  45. left: props.gap
  46. }));
  47. const dragging = ref(false);
  48. let initialized = false;
  49. const rootStyle = computed(() => {
  50. const style = {};
  51. const x = addUnit(state.value.x);
  52. const y = addUnit(state.value.y);
  53. style.transform = `translate3d(${x}, ${y}, 0)`;
  54. if (dragging.value || !initialized) {
  55. style.transition = "none";
  56. }
  57. return style;
  58. });
  59. const updateState = () => {
  60. if (!show.value) return;
  61. const {
  62. width,
  63. height
  64. } = useRect(rootRef.value);
  65. const {
  66. offset
  67. } = props;
  68. state.value = {
  69. x: offset.x > -1 ? offset.x : windowWidth.value - width - props.gap,
  70. y: offset.y > -1 ? offset.y : windowHeight.value - height - props.gap,
  71. width,
  72. height
  73. };
  74. };
  75. const touch = useTouch();
  76. let prevX = 0;
  77. let prevY = 0;
  78. const onTouchStart = (e) => {
  79. touch.start(e);
  80. dragging.value = true;
  81. prevX = state.value.x;
  82. prevY = state.value.y;
  83. };
  84. const onTouchMove = (e) => {
  85. e.preventDefault();
  86. touch.move(e);
  87. if (props.axis === "lock") return;
  88. if (!touch.isTap.value) {
  89. if (props.axis === "x" || props.axis === "xy") {
  90. let nextX = prevX + touch.deltaX.value;
  91. if (nextX < boundary.value.left) nextX = boundary.value.left;
  92. if (nextX > boundary.value.right) nextX = boundary.value.right;
  93. state.value.x = nextX;
  94. }
  95. if (props.axis === "y" || props.axis === "xy") {
  96. let nextY = prevY + touch.deltaY.value;
  97. if (nextY < boundary.value.top) nextY = boundary.value.top;
  98. if (nextY > boundary.value.bottom) nextY = boundary.value.bottom;
  99. state.value.y = nextY;
  100. }
  101. const offset = pick(state.value, ["x", "y"]);
  102. emit("update:offset", offset);
  103. }
  104. };
  105. useEventListener("touchmove", onTouchMove, {
  106. target: rootRef
  107. });
  108. const onTouchEnd = () => {
  109. dragging.value = false;
  110. nextTick(() => {
  111. if (props.magnetic === "x") {
  112. const nextX = closest([boundary.value.left, boundary.value.right], state.value.x);
  113. state.value.x = nextX;
  114. }
  115. if (props.magnetic === "y") {
  116. const nextY = closest([boundary.value.top, boundary.value.bottom], state.value.y);
  117. state.value.y = nextY;
  118. }
  119. if (!touch.isTap.value) {
  120. const offset = pick(state.value, ["x", "y"]);
  121. emit("update:offset", offset);
  122. if (prevX !== offset.x || prevY !== offset.y) {
  123. emit("offsetChange", offset);
  124. }
  125. }
  126. });
  127. };
  128. const onClick = (e) => {
  129. if (touch.isTap.value) emit("click", e);
  130. else e.stopPropagation();
  131. };
  132. onMounted(() => {
  133. updateState();
  134. nextTick(() => {
  135. initialized = true;
  136. });
  137. });
  138. watch([windowWidth, windowHeight, () => props.gap, () => props.offset], updateState, {
  139. deep: true
  140. });
  141. const show = ref(true);
  142. onActivated(() => {
  143. show.value = true;
  144. });
  145. onDeactivated(() => {
  146. if (props.teleport) {
  147. show.value = false;
  148. }
  149. });
  150. return () => {
  151. const Content = _withDirectives(_createVNode("div", _mergeProps({
  152. "class": bem(),
  153. "ref": rootRef,
  154. "onTouchstartPassive": onTouchStart,
  155. "onTouchend": onTouchEnd,
  156. "onTouchcancel": onTouchEnd,
  157. "onClickCapture": onClick,
  158. "style": rootStyle.value
  159. }, attrs), [slots.default ? slots.default() : _createVNode(Icon, {
  160. "name": props.icon,
  161. "class": bem("icon")
  162. }, null)]), [[_vShow, show.value]]);
  163. return props.teleport ? _createVNode(Teleport, {
  164. "to": props.teleport
  165. }, {
  166. default: () => [Content]
  167. }) : Content;
  168. };
  169. }
  170. });
  171. export {
  172. stdin_default as default,
  173. floatingBubbleProps
  174. };