1
// Mixins
2 1
import Delayable from '../delayable'
3 1
import Toggleable from '../toggleable'
4

5
// Utilities
6 1
import mixins from '../../util/mixins'
7 1
import { getSlot, getSlotType } from '../../util/helpers'
8 1
import { consoleError } from '../../util/console'
9

10
// Types
11
import { VNode, PropType } from 'vue'
12

13
type Listeners = Dictionary<(e: MouseEvent & KeyboardEvent & FocusEvent) => void>
14

15 1
const baseMixins = mixins(
16
  Delayable,
17
  Toggleable
18
)
19

20
/* @vue/component */
21 1
export default baseMixins.extend({
22
  name: 'activatable',
23

24
  props: {
25
    activator: {
26
      default: null as unknown as PropType<string | HTMLElement | VNode | Element | null>,
27 1
      validator: (val: string | object) => {
28 1
        return ['string', 'object'].includes(typeof val)
29
      },
30
    },
31
    disabled: Boolean,
32
    internalActivator: Boolean,
33
    openOnHover: Boolean,
34
    openOnFocus: Boolean,
35
  },
36

37 1
  data: () => ({
38
    // Do not use this directly, call getActivator() instead
39
    activatorElement: null as HTMLElement | null,
40
    activatorNode: [] as VNode[],
41
    events: ['click', 'mouseenter', 'mouseleave', 'focus'],
42
    listeners: {} as Listeners,
43
  }),
44

45
  watch: {
46
    activator: 'resetActivator',
47
    openOnFocus: 'resetActivator',
48
    openOnHover: 'resetActivator',
49
  },
50

51 1
  mounted () {
52 1
    const slotType = getSlotType(this, 'activator', true)
53

54 1
    if (slotType && ['v-slot', 'normal'].includes(slotType)) {
55 1
      consoleError(`The activator slot must be bound, try '<template v-slot:activator="{ on }"><v-btn v-on="on">'`, this)
56
    }
57

58 1
    this.addActivatorEvents()
59
  },
60

61 1
  beforeDestroy () {
62 1
    this.removeActivatorEvents()
63
  },
64

65
  methods: {
66 1
    addActivatorEvents () {
67 1
      if (
68 1
        !this.activator ||
69 1
        this.disabled ||
70 1
        !this.getActivator()
71 1
      ) return
72

73 1
      this.listeners = this.genActivatorListeners()
74 1
      const keys = Object.keys(this.listeners)
75

76 1
      for (const key of keys) {
77 1
        this.getActivator()!.addEventListener(key, this.listeners[key] as any)
78
      }
79
    },
80 1
    genActivator () {
81 1
      const node = getSlot(this, 'activator', Object.assign(this.getValueProxy(), {
82
        on: this.genActivatorListeners(),
83
        attrs: this.genActivatorAttributes(),
84 1
      })) || []
85

86 1
      this.activatorNode = node
87

88 1
      return node
89
    },
90 1
    genActivatorAttributes () {
91 1
      return {
92
        role: 'button',
93
        'aria-haspopup': true,
94
        'aria-expanded': String(this.isActive),
95
      }
96
    },
97 1
    genActivatorListeners () {
98 1
      if (this.disabled) return {}
99

100 1
      const listeners: Listeners = {}
101

102 1
      if (this.openOnHover) {
103 1
        listeners.mouseenter = (e: MouseEvent) => {
104 1
          this.getActivator(e)
105 1
          this.runDelay('open')
106
        }
107 1
        listeners.mouseleave = (e: MouseEvent) => {
108 1
          this.getActivator(e)
109 1
          this.runDelay('close')
110
        }
111
      } else {
112 1
        listeners.click = (e: MouseEvent) => {
113 1
          const activator = this.getActivator(e)
114 1
          if (activator) activator.focus()
115

116 1
          e.stopPropagation()
117

118 1
          this.isActive = !this.isActive
119
        }
120
      }
121

122 1
      if (this.openOnFocus) {
123 0
        listeners.focus = (e: FocusEvent) => {
124 0
          this.getActivator(e)
125

126 0
          e.stopPropagation()
127

128 0
          this.isActive = !this.isActive
129
        }
130
      }
131

132 1
      return listeners
133
    },
134 1
    getActivator (e?: Event): HTMLElement | null {
135
      // If we've already fetched the activator, re-use
136 1
      if (this.activatorElement) return this.activatorElement
137

138 1
      let activator = null
139

140 1
      if (this.activator) {
141 1
        const target = this.internalActivator ? this.$el : document
142

143 1
        if (typeof this.activator === 'string') {
144
          // Selector
145 1
          activator = target.querySelector(this.activator)
146 1
        } else if ((this.activator as any).$el) {
147
          // Component (ref)
148 0
          activator = (this.activator as any).$el
149
        } else {
150
          // HTMLElement | Element
151 1
          activator = this.activator
152
        }
153 1
      } else if (this.activatorNode.length === 1 || (this.activatorNode.length && !e)) {
154
        // Use the contents of the activator slot
155
        // There's either only one element in it or we
156
        // don't have a click event to use as a last resort
157 1
        const vm = this.activatorNode[0].componentInstance
158 1
        if (
159 1
          vm &&
160 0
          vm.$options.mixins && //                         Activatable is indirectly used via Menuable
161 1
          vm.$options.mixins.some((m: any) => m.options && ['activatable', 'menuable'].includes(m.options.name))
162
        ) {
163
          // Activator is actually another activatible component, use its activator (#8846)
164 0
          activator = (vm as any).getActivator()
165
        } else {
166 1
          activator = this.activatorNode[0].elm as HTMLElement
167
        }
168 1
      } else if (e) {
169
        // Activated by a click or focus event
170 1
        activator = (e.currentTarget || e.target) as HTMLElement
171
      }
172

173 1
      this.activatorElement = activator
174

175 1
      return this.activatorElement
176
    },
177 1
    getContentSlot () {
178 1
      return getSlot(this, 'default', this.getValueProxy(), true)
179
    },
180 1
    getValueProxy (): object {
181 1
      const self = this
182 1
      return {
183 1
        get value () {
184 1
          return self.isActive
185
        },
186 1
        set value (isActive: boolean) {
187 1
          self.isActive = isActive
188
        },
189
      }
190
    },
191 1
    removeActivatorEvents () {
192 1
      if (
193 1
        !this.activator ||
194 1
        !this.activatorElement
195 1
      ) return
196

197 1
      const keys = Object.keys(this.listeners)
198

199 1
      for (const key of keys) {
200 1
        (this.activatorElement as any).removeEventListener(key, this.listeners[key])
201
      }
202

203 1
      this.listeners = {}
204
    },
205 1
    resetActivator () {
206 1
      this.removeActivatorEvents()
207 1
      this.activatorElement = null
208 1
      this.getActivator()
209 1
      this.addActivatorEvents()
210
    },
211
  },
212
})

Read our documentation on viewing source code .

Loading