useActivator.mjs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. // Components
  2. import { VMenuSymbol } from "../VMenu/shared.mjs"; // Composables
  3. import { makeDelayProps, useDelay } from "../../composables/delay.mjs"; // Utilities
  4. import { computed, effectScope, inject, mergeProps, nextTick, onScopeDispose, ref, watch, watchEffect } from 'vue';
  5. import { bindProps, getCurrentInstance, IN_BROWSER, matchesSelector, propsFactory, templateRef, unbindProps } from "../../util/index.mjs"; // Types
  6. export const makeActivatorProps = propsFactory({
  7. target: [String, Object],
  8. activator: [String, Object],
  9. activatorProps: {
  10. type: Object,
  11. default: () => ({})
  12. },
  13. openOnClick: {
  14. type: Boolean,
  15. default: undefined
  16. },
  17. openOnHover: Boolean,
  18. openOnFocus: {
  19. type: Boolean,
  20. default: undefined
  21. },
  22. closeOnContentClick: Boolean,
  23. ...makeDelayProps()
  24. }, 'VOverlay-activator');
  25. export function useActivator(props, _ref) {
  26. let {
  27. isActive,
  28. isTop,
  29. contentEl
  30. } = _ref;
  31. const vm = getCurrentInstance('useActivator');
  32. const activatorEl = ref();
  33. let isHovered = false;
  34. let isFocused = false;
  35. let firstEnter = true;
  36. const openOnFocus = computed(() => props.openOnFocus || props.openOnFocus == null && props.openOnHover);
  37. const openOnClick = computed(() => props.openOnClick || props.openOnClick == null && !props.openOnHover && !openOnFocus.value);
  38. const {
  39. runOpenDelay,
  40. runCloseDelay
  41. } = useDelay(props, value => {
  42. if (value === (props.openOnHover && isHovered || openOnFocus.value && isFocused) && !(props.openOnHover && isActive.value && !isTop.value)) {
  43. if (isActive.value !== value) {
  44. firstEnter = true;
  45. }
  46. isActive.value = value;
  47. }
  48. });
  49. const cursorTarget = ref();
  50. const availableEvents = {
  51. onClick: e => {
  52. e.stopPropagation();
  53. activatorEl.value = e.currentTarget || e.target;
  54. if (!isActive.value) {
  55. cursorTarget.value = [e.clientX, e.clientY];
  56. }
  57. isActive.value = !isActive.value;
  58. },
  59. onMouseenter: e => {
  60. if (e.sourceCapabilities?.firesTouchEvents) return;
  61. isHovered = true;
  62. activatorEl.value = e.currentTarget || e.target;
  63. runOpenDelay();
  64. },
  65. onMouseleave: e => {
  66. isHovered = false;
  67. runCloseDelay();
  68. },
  69. onFocus: e => {
  70. if (matchesSelector(e.target, ':focus-visible') === false) return;
  71. isFocused = true;
  72. e.stopPropagation();
  73. activatorEl.value = e.currentTarget || e.target;
  74. runOpenDelay();
  75. },
  76. onBlur: e => {
  77. isFocused = false;
  78. e.stopPropagation();
  79. runCloseDelay();
  80. }
  81. };
  82. const activatorEvents = computed(() => {
  83. const events = {};
  84. if (openOnClick.value) {
  85. events.onClick = availableEvents.onClick;
  86. }
  87. if (props.openOnHover) {
  88. events.onMouseenter = availableEvents.onMouseenter;
  89. events.onMouseleave = availableEvents.onMouseleave;
  90. }
  91. if (openOnFocus.value) {
  92. events.onFocus = availableEvents.onFocus;
  93. events.onBlur = availableEvents.onBlur;
  94. }
  95. return events;
  96. });
  97. const contentEvents = computed(() => {
  98. const events = {};
  99. if (props.openOnHover) {
  100. events.onMouseenter = () => {
  101. isHovered = true;
  102. runOpenDelay();
  103. };
  104. events.onMouseleave = () => {
  105. isHovered = false;
  106. runCloseDelay();
  107. };
  108. }
  109. if (openOnFocus.value) {
  110. events.onFocusin = () => {
  111. isFocused = true;
  112. runOpenDelay();
  113. };
  114. events.onFocusout = () => {
  115. isFocused = false;
  116. runCloseDelay();
  117. };
  118. }
  119. if (props.closeOnContentClick) {
  120. const menu = inject(VMenuSymbol, null);
  121. events.onClick = () => {
  122. isActive.value = false;
  123. menu?.closeParents();
  124. };
  125. }
  126. return events;
  127. });
  128. const scrimEvents = computed(() => {
  129. const events = {};
  130. if (props.openOnHover) {
  131. events.onMouseenter = () => {
  132. if (firstEnter) {
  133. isHovered = true;
  134. firstEnter = false;
  135. runOpenDelay();
  136. }
  137. };
  138. events.onMouseleave = () => {
  139. isHovered = false;
  140. runCloseDelay();
  141. };
  142. }
  143. return events;
  144. });
  145. watch(isTop, val => {
  146. if (val && (props.openOnHover && !isHovered && (!openOnFocus.value || !isFocused) || openOnFocus.value && !isFocused && (!props.openOnHover || !isHovered)) && !contentEl.value?.contains(document.activeElement)) {
  147. isActive.value = false;
  148. }
  149. });
  150. watch(isActive, val => {
  151. if (!val) {
  152. setTimeout(() => {
  153. cursorTarget.value = undefined;
  154. });
  155. }
  156. }, {
  157. flush: 'post'
  158. });
  159. const activatorRef = templateRef();
  160. watchEffect(() => {
  161. if (!activatorRef.value) return;
  162. nextTick(() => {
  163. activatorEl.value = activatorRef.el;
  164. });
  165. });
  166. const targetRef = templateRef();
  167. const target = computed(() => {
  168. if (props.target === 'cursor' && cursorTarget.value) return cursorTarget.value;
  169. if (targetRef.value) return targetRef.el;
  170. return getTarget(props.target, vm) || activatorEl.value;
  171. });
  172. const targetEl = computed(() => {
  173. return Array.isArray(target.value) ? undefined : target.value;
  174. });
  175. let scope;
  176. watch(() => !!props.activator, val => {
  177. if (val && IN_BROWSER) {
  178. scope = effectScope();
  179. scope.run(() => {
  180. _useActivator(props, vm, {
  181. activatorEl,
  182. activatorEvents
  183. });
  184. });
  185. } else if (scope) {
  186. scope.stop();
  187. }
  188. }, {
  189. flush: 'post',
  190. immediate: true
  191. });
  192. onScopeDispose(() => {
  193. scope?.stop();
  194. });
  195. return {
  196. activatorEl,
  197. activatorRef,
  198. target,
  199. targetEl,
  200. targetRef,
  201. activatorEvents,
  202. contentEvents,
  203. scrimEvents
  204. };
  205. }
  206. function _useActivator(props, vm, _ref2) {
  207. let {
  208. activatorEl,
  209. activatorEvents
  210. } = _ref2;
  211. watch(() => props.activator, (val, oldVal) => {
  212. if (oldVal && val !== oldVal) {
  213. const activator = getActivator(oldVal);
  214. activator && unbindActivatorProps(activator);
  215. }
  216. if (val) {
  217. nextTick(() => bindActivatorProps());
  218. }
  219. }, {
  220. immediate: true
  221. });
  222. watch(() => props.activatorProps, () => {
  223. bindActivatorProps();
  224. });
  225. onScopeDispose(() => {
  226. unbindActivatorProps();
  227. });
  228. function bindActivatorProps() {
  229. let el = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getActivator();
  230. let _props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : props.activatorProps;
  231. if (!el) return;
  232. bindProps(el, mergeProps(activatorEvents.value, _props));
  233. }
  234. function unbindActivatorProps() {
  235. let el = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getActivator();
  236. let _props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : props.activatorProps;
  237. if (!el) return;
  238. unbindProps(el, mergeProps(activatorEvents.value, _props));
  239. }
  240. function getActivator() {
  241. let selector = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : props.activator;
  242. const activator = getTarget(selector, vm);
  243. // The activator should only be a valid element (Ignore comments and text nodes)
  244. activatorEl.value = activator?.nodeType === Node.ELEMENT_NODE ? activator : undefined;
  245. return activatorEl.value;
  246. }
  247. }
  248. function getTarget(selector, vm) {
  249. if (!selector) return;
  250. let target;
  251. if (selector === 'parent') {
  252. let el = vm?.proxy?.$el?.parentNode;
  253. while (el?.hasAttribute('data-no-activator')) {
  254. el = el.parentNode;
  255. }
  256. target = el;
  257. } else if (typeof selector === 'string') {
  258. // Selector
  259. target = document.querySelector(selector);
  260. } else if ('$el' in selector) {
  261. // Component (ref)
  262. target = selector.$el;
  263. } else {
  264. // HTMLElement | Element | [x, y]
  265. target = selector;
  266. }
  267. return target;
  268. }
  269. //# sourceMappingURL=useActivator.mjs.map