1'use strict'; 2 3document.addEventListener('alpine:init', () => { 4 /** 5 * Action Dropdown Component 6 * 7 * A reusable dropdown menu component for table actions (Edit, Delete, etc.) 8 * Follows Hyvä Themes and Alpine.js 3 best practices. 9 * 10 * @example Basic usage: 11 * <td x-data="actionDropdown()"> 12 * <template x-ref="triggerTemplate"> 13 * <button x-bind="trigger" class="text-gray-400 hover:text-gray-600"> 14 * <!-- Icon here --> 15 * </button> 16 * </template> 17 * <template x-ref="menuTemplate"> 18 * <a href="#" @click="close()" class="dropdown-item">Edit</a> 19 * <a href="#" @click="close()" class="dropdown-item">Delete</a> 20 * </template> 21 * </td> 22 * 23 * @example With custom actions: 24 * <div x-data="actionDropdown({ onOpen: () => console.log('opened') })"> 25 * ... 26 * </div> 27 */ 28 Alpine.data('actionDropdown', (config = {}) => ({ 29 isOpen: false, 30 31 init() { 32 if (config.onInit && typeof config.onInit === 'function') { 33 config.onInit.call(this); 34 } 35 }, 36 37 trigger: { 38 ['@click']() { 39 this.toggle(); 40 }, 41 ['@click.away']() { 42 this.close(); 43 }, 44 ['@keydown.escape.window']() { 45 this.close(); 46 }, 47 [':aria-expanded']() { 48 return this.isOpen; 49 } 50 }, 51 52 menu: { 53 ['x-show']() { 54 return this.isOpen; 55 }, 56 ['x-transition:enter']: 'transition ease-out duration-100', 57 ['x-transition:enter-start']: 'opacity-0 scale-95', 58 ['x-transition:enter-end']: 'opacity-100 scale-100', 59 ['x-transition:leave']: 'transition ease-in duration-75', 60 ['x-transition:leave-start']: 'opacity-100 scale-100', 61 ['x-transition:leave-end']: 'opacity-0 scale-95', 62 ['style']: 'display: none;' 63 }, 64 65 toggle() { 66 this.isOpen = !this.isOpen; 67 if (this.isOpen) { 68 this.$nextTick(() => { 69 this.scrollIntoViewIfNeeded(); 70 if (config.onOpen && typeof config.onOpen === 'function') { 71 config.onOpen.call(this); 72 } 73 }); 74 } 75 }, 76 77 open() { 78 if (!this.isOpen) { 79 this.isOpen = true; 80 this.$nextTick(() => { 81 this.scrollIntoViewIfNeeded(); 82 if (config.onOpen && typeof config.onOpen === 'function') { 83 config.onOpen.call(this); 84 } 85 }); 86 } 87 }, 88 89 close() { 90 if (this.isOpen) { 91 this.isOpen = false; 92 if (config.onClose && typeof config.onClose === 'function') { 93 config.onClose.call(this); 94 } 95 } 96 }, 97 98 scrollIntoViewIfNeeded() { 99 if (this.$refs.menu) { 100 this.$refs.menu.scrollIntoView({ 101 behavior: 'smooth', 102 block: 'nearest' 103 }); 104 } 105 } 106 })); 107 108 /** 109 * Table Action Menu Component 110 * 111 * Simplified version specifically for table row actions. 112 * Pre-configured with common defaults for table dropdowns. 113 * 114 * @example 115 * <td class="relative" x-data="tableActionMenu()"> 116 * <button x-bind="trigger">...</button> 117 * <div x-ref="menu" x-bind="menu" class="absolute right-0 mt-2 ..."> 118 * <button @click="action('edit', itemData)">Edit</button> 119 * <button @click="action('delete', itemData)">Delete</button> 120 * </div> 121 * </td> 122 */ 123 Alpine.data('tableActionMenu', (config = {}) => ({ 124 isOpen: false, 125 itemData: config.itemData || null, 126 127 trigger: { 128 ['@click']() { 129 this.isOpen = !this.isOpen; 130 if (this.isOpen) { 131 this.$nextTick(() => { 132 if (this.$refs.menu) { 133 this.$refs.menu.scrollIntoView({ 134 behavior: 'smooth', 135 block: 'nearest' 136 }); 137 } 138 }); 139 } 140 }, 141 ['@click.away']() { 142 this.isOpen = false;
143 }, 144 ['@keydown.escape.window']() { 145 this.isOpen = false; 146 } 147 }, 148 149 menu: { 150 ['x-show']() { 151 return this.isOpen; 152 }, 153 ['x-transition:enter']: 'transition ease-out duration-100', 154 ['x-transition:enter-start']: 'opacity-0 scale-95', 155 ['x-transition:enter-end']: 'opacity-100 scale-100', 156 ['x-transition:leave']: 'transition ease-in duration-75', 157 ['x-transition:leave-start']: 'opacity-100 scale-100', 158 ['x-transition:leave-end']: 'opacity-0 scale-95', 159 ['style']: 'display: none;' 160 }, 161 162 action(actionType, data) { 163 this.isOpen = false; 164 const eventName = config.eventPrefix 165 ? `${config.eventPrefix}-${actionType}` 166 : `open-${actionType}-modal`; 167 168 this.$dispatch(eventName, { [config.dataKey || 'data']: data || this.itemData }); 169 }, 170 171 close() { 172 this.isOpen = false; 173 } 174 })); 175});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.