useActivator.mjs 6.8 KB

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