boxSelect.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. <template>
  2. <div
  3. class="box-select__container"
  4. @mousedown.left="mouseDown"
  5. @mousemove.stop="mouseMove"
  6. :class="uuid"
  7. >
  8. <div
  9. class="box-select__coordinate"
  10. :style="style"
  11. ref="selectContainer"
  12. ></div>
  13. <slot></slot>
  14. </div>
  15. </template>
  16. <script>
  17. import { debounce, isNumber } from "lodash";
  18. import { ref, onUnmounted, nextTick, shallowRef } from "vue";
  19. /**
  20. * @description 判断元素是否在范围内
  21. * @param {Object} dom dom元素
  22. */
  23. const isWithinRange = (dom, top, bottom, left, right) => {
  24. const eleRect = dom.getBoundingClientRect();
  25. return !(
  26. eleRect.top > bottom ||
  27. eleRect.bottom < top ||
  28. eleRect.right < left ||
  29. eleRect.left > right
  30. );
  31. };
  32. export default {
  33. name: "BoxSelect",
  34. /**
  35. * @member props
  36. * @property {String} [node] 要框选的元素,可以是元素名,也可以是class名, 也可以是id名
  37. * @property {String} [selectedClass] 已选中元素附加的class名
  38. */
  39. props: {
  40. node: {
  41. required: true,
  42. type: String,
  43. },
  44. selectedClass: {
  45. type: String,
  46. default: "box-select__hypocritical",
  47. },
  48. },
  49. // 鼠标按下
  50. emits: ["mouseUp", "mouseDown"],
  51. setup(props, { emit }) {
  52. let top = 0,
  53. left = 0,
  54. width = 0,
  55. height = 0,
  56. startX = 0,
  57. startY = 0,
  58. timer = null,
  59. // 记录是框选还是点击
  60. mouseOn = false;
  61. const style = ref({}),
  62. selectContainer = ref(null),
  63. // 给当前框容器加一个唯一识别符, 以保证所选择到的元素都是当前容器的. 否则会选择到容器外同名的元素
  64. uuid = shallowRef("uuid_" + new Date().valueOf());
  65. const query = (className = "") => {
  66. let domName = `.${uuid.value} ${props.node}`;
  67. className && (domName += `.${className}`);
  68. return Array.from(document.querySelectorAll(domName) || []);
  69. };
  70. const classOperation = (ele, method = "add", className = "") =>
  71. ele.classList[method](className);
  72. const setStyle = (styles = {}, newStyles = {}) => {
  73. Object.keys(styles).map((item) => {
  74. newStyles[item] = styles[item] + (isNumber(styles[item]) ? "px" : "");
  75. });
  76. style.value = newStyles;
  77. };
  78. const getAreaWithinElements = () => {
  79. const { bottom, left, right, top } =
  80. selectContainer.value.getBoundingClientRect();
  81. // 所有可框选元素
  82. const elements = query();
  83. // // 已选中元素
  84. // const selectedElements = elements.filter((item) =>
  85. // classOperation(item, "contains", props.selectedClass)
  86. // );
  87. // // 未选中元素
  88. // const unselectedElements = elements.filter(
  89. // (item) => !classOperation(item, "contains", props.selectedClass)
  90. // );
  91. // 本次框选元素
  92. let selctList = [];
  93. for (const i in elements) {
  94. if (Number(i) || i === "0") {
  95. let val = elements[i];
  96. const withinRange = isWithinRange(val, top, bottom, left, right);
  97. if (withinRange) {
  98. selctList.push(val);
  99. }
  100. }
  101. }
  102. emit("selectList", selctList);
  103. // selectedElements.map((item) => {
  104. // const withinRange = isWithinRange(item, top, bottom, left, right);
  105. // withinRange &&
  106. // classOperation(item, "add", props.selectedClass)
  107. // });
  108. // unselectedElements.map(
  109. // (item) =>
  110. // isWithinRange(item, top, bottom, left, right) &&
  111. // classOperation(item, "add", props.selectedClass)
  112. // );
  113. // return query(props.selectedClass);
  114. };
  115. const mouseDown = debounce((event) => {
  116. timer = setTimeout(() => {
  117. mouseOn = true;
  118. startX = event.clientX;
  119. startY = event.clientY;
  120. emit("mouseDown");
  121. }, 300);
  122. // 重置本次框选的元素列表
  123. setStyle({
  124. left,
  125. startX,
  126. top: startY,
  127. width: 0,
  128. height: 0,
  129. display: "block",
  130. });
  131. });
  132. const mouseMove = debounce((event) => {
  133. if (!mouseOn) return false;
  134. const _width = event.clientX - startX;
  135. const _height = event.clientY - startY;
  136. top = _height > 0 ? startY : event.clientY;
  137. left = _width > 0 ? startX : event.clientX;
  138. width = Math.abs(_width);
  139. height = Math.abs(_height);
  140. setStyle({ left, top, width, height });
  141. });
  142. const mouseUp = debounce((event) => {
  143. timer && clearTimeout(timer);
  144. // 判断是否鼠标左键
  145. if (event.which !== 1) return false;
  146. // 判断是框选还是点击
  147. if (!mouseOn) return false;
  148. mouseOn = false;
  149. setStyle({ display: "none" });
  150. // 获得已选中的元素
  151. const selectedEles = getAreaWithinElements();
  152. // 响应事件,并传递本次框选的元素列表
  153. emit("mouseUp", selectedEles);
  154. });
  155. nextTick(() => document.addEventListener("mouseup", mouseUp));
  156. onUnmounted(() => document.removeEventListener("mouseup", mouseUp));
  157. return {
  158. mouseUp,
  159. mouseDown,
  160. mouseMove,
  161. timer,
  162. style,
  163. selectContainer,
  164. uuid,
  165. };
  166. },
  167. };
  168. </script>
  169. <style lang="scss">
  170. .box-select__container {
  171. // height: 50vh;
  172. }
  173. .box-select__coordinate {
  174. position: fixed;
  175. z-index: 11;
  176. left: 0;
  177. top: 0;
  178. width: 0;
  179. height: 0;
  180. background: rgb(106, 90, 205,.5);
  181. border: 1px solid blue;
  182. opacity: 0.6;
  183. pointer-events: none;
  184. }
  185. .box-select__hypocritical {
  186. background-color: blue;
  187. }
  188. </style>