@vue-interface/btn-dropdown 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@vue-interface/btn-dropdown",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A Vue button dropdown component.",
5
5
  "files": [
6
6
  "dist",
7
- "tailwindcss"
7
+ "src"
8
8
  ],
9
9
  "main": "./dist/btn-dropdown.umd.js",
10
- "module": "./dist/btn-dropdown.es.js",
10
+ "module": "./index.js",
11
11
  "browserslist": "last 2 versions, > 0.5%, ie >= 11",
12
12
  "scripts": {
13
13
  "dev": "vite",
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <component
3
+ :is="$attrs.split === undefined || !!$attrs.nav ? 'btn-dropdown-single' : 'btn-dropdown-split'"
4
+ class="btn-dropdown"
5
+ v-bind="$attrs"
6
+ @click="(...args) => this.$emit('click', ...args)"
7
+ @click-toggle="(...args) => this.$emit('click-toggle', ...args)"
8
+ @dropdown="(...args) => this.$emit('dropdown', ...args)"
9
+ @show="(...args) => this.$emit('show', ...args)"
10
+ @hide="(...args) => this.$emit('hide', ...args)"
11
+ @toggle="(...args) => this.$emit('toggle', ...args)">
12
+ <template #icon>
13
+ <slot name="icon" />
14
+ </template>
15
+ <template v-if="$attrs.label || this.$slots.label" #label>
16
+ <slot name="label">
17
+ {{ $attrs.label }}
18
+ </slot>
19
+ </template>
20
+ <template #button="slot">
21
+ <slot name="button" v-bind="slot" />
22
+ </template>
23
+ <template #split="slot">
24
+ <slot name="split" v-bind="slot" />
25
+ </template>
26
+ <slot />
27
+ </component>
28
+ </template>
29
+
30
+ <script>
31
+ import BtnDropdownSplit from './BtnDropdownSplit.vue';
32
+ import BtnDropdownSingle from './BtnDropdownSingle.vue';
33
+
34
+ export default {
35
+
36
+ name: 'BtnDropdown',
37
+
38
+ components: {
39
+ BtnDropdownSplit,
40
+ BtnDropdownSingle
41
+ },
42
+
43
+ inheritAttrs: false
44
+
45
+ };
46
+ </script>
47
+
48
+ <style>
49
+ @keyframes btnDropdownZoomIn {
50
+ from {
51
+ opacity: 0;
52
+ }
53
+
54
+ to {
55
+ opacity: 1;
56
+ }
57
+ }
58
+
59
+
60
+ .btn-dropdown {
61
+ position: relative;
62
+ }
63
+
64
+ .btn-dropdown .dropdown-toggle {
65
+ display: flex;
66
+ transition: all 125ms ease-in;
67
+ align-items: center;
68
+ justify-content: center;
69
+ }
70
+
71
+ .nav-item .btn-group,
72
+ .nav-item .btn-dropdown .dropdown-toggle {
73
+ display: block;
74
+ }
75
+
76
+ .btn-dropdown.rounded-circle > .btn:last-child,
77
+ .btn-dropdown.rounded-circle > .btn-group:last-child .dropdown-toggle {
78
+ border-top-right-radius: 100%;
79
+ border-bottom-right-radius: 100%;
80
+ }
81
+
82
+ .btn-dropdown.rounded-circle > .btn:first-child,
83
+ .btn-dropdown.rounded-circle > .btn-group:first-child .dropdown-toggle {
84
+ border-top-left-radius: 100%;
85
+ border-bottom-left-radius: 100%;
86
+ }
87
+
88
+ .btn-dropdown .rounded-circle {
89
+ border-radius: 100%;
90
+ }
91
+
92
+ .btn-dropdown .rotate-90 {
93
+ transform: rotate(90deg);
94
+ }
95
+
96
+ .btn-dropdown.hide-caret .dropdown-toggle::after,
97
+ .btn-dropdown.icon-only .dropdown-toggle::after,
98
+ .btn-dropdown.hide-caret .dropdown-toggle::before,
99
+ .btn-dropdown.icon-only .dropdown-toggle::before {
100
+ display: none;
101
+ }
102
+
103
+ .btn-dropdown .dropdown-menu {
104
+ animation-timing-function: ease-in-out;
105
+ animation-duration: 200ms;
106
+ animation-fill-mode: both;
107
+ }
108
+
109
+ .btn-dropdown .dropdown-menu.animated {
110
+ animation-name: btnDropdownZoomIn;
111
+ }
112
+ </style>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <component
3
+ :is="is"
4
+ :id="id"
5
+ v-bind="to ? { to } : { href }"
6
+ aria-haspopup="true"
7
+ :aria-expanded="expanded"
8
+ :type="is === 'button' ? 'button': undefined">
9
+ <slot />
10
+ </component>
11
+ </template>
12
+
13
+ <script>
14
+ export default {
15
+ props: {
16
+ expanded: {
17
+ type: Boolean,
18
+ default: false
19
+ },
20
+ id: String,
21
+ href: String,
22
+ to: [String, Object]
23
+ },
24
+ computed: {
25
+ is() {
26
+ if(this.to) {
27
+ return 'router-link';
28
+ }
29
+
30
+ if(this.href) {
31
+ return 'a';
32
+ }
33
+
34
+ return 'button';
35
+ },
36
+ }
37
+ };
38
+ </script>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <btn-group :class="classes">
3
+ <slot name="button" v-bind="this">
4
+ <btn-dropdown-action
5
+ :id="$attrs.id"
6
+ ref="button"
7
+ :expanded="expanded"
8
+ :href="href"
9
+ :to="to"
10
+ :style="toggleStyle"
11
+ :class="toggleClasses"
12
+ @blur.native="onBlur"
13
+ @click.native="onClickToggle">
14
+ <slot name="icon" />
15
+ <slot name="label">
16
+ {{ label }}
17
+ </slot>
18
+ </btn-dropdown-action>
19
+ </slot>
20
+ <dropdown-menu
21
+ :id="$attrs.id"
22
+ ref="menu"
23
+ :align="align"
24
+ :show="expanded"
25
+ :class="{animated: triggerAnimation}"
26
+ @blur-item="onBlur"
27
+ @click-item="onClickItem">
28
+ <slot />
29
+ </dropdown-menu>
30
+ </btn-group>
31
+ </template>
32
+
33
+ <script>
34
+ import DropdownHandler from './DropdownHandler';
35
+
36
+ export default {
37
+
38
+ mixins: [
39
+ DropdownHandler
40
+ ]
41
+
42
+ };
43
+ </script>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <btn-group :class="classes" class="btn-dropdown-split" @click="onClick">
3
+ <slot v-if="!dropleft" name="button" v-bind="this">
4
+ <btn-dropdown-action
5
+ v-if="!dropleft"
6
+ :id="$attrs.id"
7
+ ref="button"
8
+ :expanded="expanded"
9
+ :href="href"
10
+ :to="to"
11
+ :class="actionClasses"
12
+ @click.native="e => $emit('click', e)">
13
+ <slot name="icon" />
14
+ <slot name="label">
15
+ {{ label }}
16
+ </slot>
17
+ </btn-dropdown-action>
18
+ </slot>
19
+
20
+ <btn-group ref="split">
21
+ <slot name="split" v-bind="this">
22
+ <button
23
+ v-if="split"
24
+ :id="$attrs.id"
25
+ type="button"
26
+ aria-haspopup="true"
27
+ :aria-expanded="expanded"
28
+ :class="toggleClasses"
29
+ @blur="onBlur"
30
+ @click="onClickToggle" />
31
+ </slot>
32
+
33
+ <dropdown-menu
34
+ :id="$attrs.id"
35
+ ref="menu"
36
+ :align="align"
37
+ :show="expanded"
38
+ :class="{animated: triggerAnimation}"
39
+ @click-item="onClickItem"
40
+ @blur-item="onBlur">
41
+ <slot />
42
+ </dropdown-menu>
43
+ </btn-group>
44
+ <slot v-if="dropleft" name="button" v-bind="this">
45
+ <btn-dropdown-action
46
+ v-if="dropleft"
47
+ :id="$attrs.id"
48
+ ref="button"
49
+ :expanded="expanded"
50
+ :href="href"
51
+ :to="to"
52
+ :class="actionClasses"
53
+ @click.native="e => $emit('click', e)">
54
+ <slot name="icon" />
55
+ <slot name="label">
56
+ {{ label }}
57
+ </slot>
58
+ </btn-dropdown-action>
59
+ </slot>
60
+ </btn-group>
61
+ </template>
62
+
63
+ <script>
64
+ import DropdownHandler from './DropdownHandler';
65
+
66
+ export default {
67
+
68
+ mixins: [
69
+ DropdownHandler
70
+ ]
71
+
72
+ };
73
+ </script>
@@ -0,0 +1,421 @@
1
+ import { createPopper } from '@popperjs/core';
2
+ import { Btn } from '@vue-interface/btn';
3
+ import { BtnGroup } from '@vue-interface/btn-group';
4
+ import { DropdownMenu } from '@vue-interface/dropdown-menu';
5
+ import BtnDropdownAction from './BtnDropdownAction.vue';
6
+
7
+ const TAB_KEYCODE = 9;
8
+
9
+ export default {
10
+
11
+ components: {
12
+ BtnDropdownAction,
13
+ BtnGroup,
14
+ DropdownMenu
15
+ },
16
+
17
+ extends: Btn,
18
+
19
+ props: {
20
+
21
+ /**
22
+ * Display the dropdown menu aligned left or right
23
+ *
24
+ * @property String
25
+ */
26
+ align: {
27
+ type: String,
28
+ default: 'left',
29
+ validate(value) {
30
+ return ['left', 'right'].indexOf(value.toLowerCase()) !== -1;
31
+ }
32
+ },
33
+
34
+ /**
35
+ * Should animate the dropdown opening.
36
+ *
37
+ * @property {Boolean}
38
+ */
39
+ animated: {
40
+ type: Boolean,
41
+ default: true
42
+ },
43
+
44
+ // buttonClass: String,
45
+
46
+ /**
47
+ * Show the caret.
48
+ *
49
+ * @property {Boolean}
50
+ */
51
+ caret: {
52
+ type: Boolean,
53
+ default: true
54
+ },
55
+
56
+ /**
57
+ * Should display the toggle button as a circle.
58
+ *
59
+ * @property Boolean
60
+ */
61
+
62
+ // circle: {
63
+ // type: Boolean,
64
+ // default: false
65
+ // },
66
+
67
+ /**
68
+ * Display as a dropup instead of a dropdown.
69
+ *
70
+ * @property Boolean
71
+ */
72
+ dropup: {
73
+ type: Boolean,
74
+ default: false
75
+ },
76
+
77
+ /**
78
+ * Display as a dropright instead of a dropdown.
79
+ *
80
+ * @property Boolean
81
+ */
82
+ dropright: {
83
+ type: Boolean,
84
+ default: false
85
+ },
86
+
87
+ /**
88
+ * Display as a dropleft instead of a dropdown.
89
+ *
90
+ * @property Boolean
91
+ */
92
+ dropleft: {
93
+ type: Boolean,
94
+ default: false
95
+ },
96
+
97
+ /**
98
+ * The action height.
99
+ *
100
+ * @property {String}
101
+ */
102
+ height: String,
103
+
104
+ /**
105
+ * The href action.
106
+ *
107
+ * @property {String}
108
+ */
109
+ href: String,
110
+
111
+ /**
112
+ * Is the dropdown a nav item?
113
+ *
114
+ * @property {Boolean}
115
+ */
116
+ nav: Boolean,
117
+
118
+ /**
119
+ * The toggle button's label. If not defined as an attribute,
120
+ * you can override with the component's slot (inner html).
121
+ *
122
+ * @property {String}
123
+ */
124
+ label: String,
125
+
126
+ offset: {
127
+ type: Number,
128
+ default: 5,
129
+ },
130
+
131
+ /**
132
+ * Should rotate the toggle button when opened.
133
+ *
134
+ * @property {Boolean}
135
+ */
136
+ rotate: {
137
+ type: Boolean,
138
+ default: false
139
+ },
140
+
141
+ /**
142
+ * Display the dropdown button with a split toggle button.
143
+ *
144
+ * @property {Boolean}
145
+ */
146
+ split: {
147
+ type: Boolean,
148
+ default: false
149
+ },
150
+
151
+ /**
152
+ * The "to" path, used for vue-router.
153
+ *
154
+ * @property {String|Object}
155
+ */
156
+ to: [String, Object],
157
+
158
+ /**
159
+ * The button type attribute.
160
+ *
161
+ * @property {String}
162
+ */
163
+ type: {
164
+ type: String,
165
+ default: 'button'
166
+ },
167
+
168
+ /**
169
+ * The action width.
170
+ *
171
+ * @property {String}
172
+ */
173
+ width: String,
174
+
175
+ },
176
+
177
+ data() {
178
+ return {
179
+ popper: null,
180
+ triggerAnimation: false,
181
+ expanded: false
182
+ };
183
+ },
184
+
185
+ computed: {
186
+ placement() {
187
+ if(this.dropup) {
188
+ return 'top';
189
+ }
190
+
191
+ if(this.dropleft) {
192
+ return 'left';
193
+ }
194
+
195
+ if(this.dropright) {
196
+ return 'right';
197
+ }
198
+
199
+ return 'bottom';
200
+ },
201
+
202
+ variantClassPrefix() {
203
+ return 'btn' + (this.outline ? '-outline' : '');
204
+ },
205
+
206
+ sizeableClassPrefix() {
207
+ return 'btn';
208
+ },
209
+
210
+ classes() {
211
+ return {
212
+ 'dropdown': this.dropup && this.dropright && this.dropleft,
213
+ 'dropup': this.dropup,
214
+ 'dropright': this.dropright,
215
+ 'dropleft': this.dropleft,
216
+ 'icon-only': !this.nav && !this.split && !!this.$slots.icon && !this.$slots.label,
217
+ 'hide-caret': !this.caret,
218
+ 'expanded': this.expanded,
219
+ // 'rounded-circle': !this.nav && this.split && this.circle,
220
+ 'rotate-90': !this.nav && this.split && this.rotate && this.expanded,
221
+ };
222
+ },
223
+
224
+ actionClasses() {
225
+ return [
226
+ !this.nav && 'btn',
227
+ !this.nav && this.size && this.sizeableClass,
228
+ !this.nav && this.variant && this.variantClass,
229
+ ]
230
+ .filter(value => !!value)
231
+ .join(' ');
232
+ },
233
+
234
+ toggleStyle() {
235
+ return {
236
+ width: this.width,
237
+ height: this.height,
238
+ };
239
+ },
240
+
241
+ toggleClasses() {
242
+ return [
243
+ // this.buttonClass,
244
+ this.nav && 'nav-link',
245
+ !this.nav && 'btn',
246
+ !this.nav && this.variantClass,
247
+ this.sizeableClass,
248
+ this.active ? 'active' : '',
249
+ this.block ? 'btn-block' : '',
250
+ // !this.split && this.circle ? 'rounded-circle p-0' : '',
251
+ !this.split && this.rotate && this.expanded ? 'rotate-90' : '',
252
+ !this.nav && this.split ? 'dropdown-toggle-split' : '',
253
+ 'dropdown-toggle',
254
+ ]
255
+ .filter(value => !!value)
256
+ .join(' ');
257
+ }
258
+ },
259
+
260
+ beforeDestroy() {
261
+ this.popper && this.popper.destroy();
262
+ },
263
+
264
+ mounted() {
265
+ // const toggle = this.$el.querySelector('.dropdown-toggle');
266
+
267
+ // toggle.addEventListener('click', () => {
268
+ // if(!this.expanded) {
269
+ // toggle.blur();
270
+ // }
271
+ // });
272
+
273
+ // toggle.addEventListener('blur', this.onBlurItem);
274
+
275
+ // const menu = this.$el.querySelector('.dropdown-menu');
276
+
277
+ // menu.addEventListener('click', e => {
278
+ // if(e.target === menu) {
279
+ // toggle.focus();
280
+ // }
281
+ // });
282
+ },
283
+
284
+ methods: {
285
+
286
+ /**
287
+ * Focus on the the dropdown toggle button
288
+ *
289
+ * @return void
290
+ */
291
+ focus() {
292
+ this.$el.querySelector('.dropdown-toggle').focus();
293
+ },
294
+
295
+ /**
296
+ * Focus on the the dropdown toggle button
297
+ *
298
+ * @return void
299
+ */
300
+ queryFocusable() {
301
+ return this.$el.querySelector('.dropdown-menu').querySelectorAll('label, input, select, textarea, [tabindex]:not([tabindex="-1"])');
302
+ },
303
+
304
+ /**
305
+ * Method to check if the given element is focusable.
306
+ *
307
+ * @return void
308
+ */
309
+ isFocusable(element) {
310
+ const nodes = this.queryFocusable();
311
+
312
+ for(let i in nodes) {
313
+ if(element === nodes[i]) {
314
+ return true;
315
+ }
316
+ }
317
+
318
+ return false;
319
+ },
320
+
321
+ /**
322
+ * Toggle the dropdown menu
323
+ *
324
+ * @return void
325
+ */
326
+ toggle(e) {
327
+ !this.expanded ? this.show() : this.hide();
328
+ },
329
+
330
+ /**
331
+ * Show the dropdown menu
332
+ *
333
+ * @return void
334
+ */
335
+ show() {
336
+ this.expanded = true;
337
+
338
+ const target = this.$refs.split && this.$refs.split.$el || this.$el;
339
+
340
+ // Hack for popper for align="right"
341
+ // this.$refs.menu.$el.style.left = 'auto';
342
+ // this.$refs.menu.$el.style.right = 'auto';
343
+
344
+ if(!this.nav && !this.popper) {
345
+ this.popper = createPopper(target, this.$refs.menu.$el, {
346
+ placement: `${this.placement}-${this.align === 'left' ? 'start' : 'end'}`,
347
+ onFirstUpdate: () => {
348
+ this.triggerAnimation = this.animated;
349
+ },
350
+ modifiers: [
351
+ {
352
+ name: 'offset',
353
+ options: {
354
+ offset: [0, !this.nav ? this.offset : 1]
355
+ // offset: ['.125rem', !this.nav ? 4 : 1],
356
+ },
357
+ },
358
+ ]
359
+ });
360
+ }
361
+ else if(this.popper) {
362
+ this.popper.update();
363
+ }
364
+ },
365
+
366
+ /**
367
+ * Hide the dropdown menu
368
+ *
369
+ * @return void
370
+ */
371
+ hide() {
372
+ this.expanded = false;
373
+ },
374
+
375
+ /**
376
+ * A callback function for the `blur-item` event.
377
+ *
378
+ * @return void
379
+ */
380
+ onBlur(e) {
381
+ if(!this.$el.contains(e.relatedTarget)) {
382
+ this.hide();
383
+ }
384
+ },
385
+
386
+ /**
387
+ * A callback function for the `click-item` event.
388
+ *
389
+ * @return void
390
+ */
391
+ onClickItem(e) {
392
+ if(!this.isFocusable(e.target)) {
393
+ this.hide();
394
+ }
395
+ },
396
+
397
+ /**
398
+ * A callback function for the `click-toggle` event.
399
+ *
400
+ * @return void
401
+ */
402
+ onClickToggle(e) {
403
+ this.$emit('click-toggle', e);
404
+
405
+ if(!e.defaultPrevented) {
406
+ this.toggle();
407
+ }
408
+ }
409
+
410
+ },
411
+
412
+ watch: {
413
+ expanded(value) {
414
+ this.$nextTick(() => {
415
+ this.$emit(value ? 'show' : 'hide');
416
+ this.$emit('toggle', value);
417
+ });
418
+ }
419
+ }
420
+
421
+ };