Caleb Porzio 1 year ago
parent
commit
e67d428036

+ 14 - 283
index.html

@@ -5,292 +5,23 @@
     <script src="./packages/focus/dist/cdn.js"></script>
     <script src="./packages/mask/dist/cdn.js"></script>
     <script src="./packages/ui/dist/cdn.js" defer></script> -->
+    <script src="./packages/anchor/dist/cdn.js" defer></script>
     <script src="./packages/alpinejs/dist/cdn.js" defer></script>
     <!-- <script src="//cdn.tailwindcss.com"></script> -->
     <!-- <script src="//cdn.tailwindcss.com"></script> -->
 
-    <div x-data="{
-        my_array: [{x:'x'},{x:'y'}],
-        click() {
-            this.my_array = [{x:'a'},{x:'b'}];
-        }
-    }">
-
-        <!-- Loop with plain div -->
-        <template x-for="item in my_array">
-            <div x-text="item.x"></div>
-        </template>
-
-        <!-- Loop with div nested inside component -->
-        <template x-for="item in my_array">
-            <div x-data="some_component" >
-                <div x-text="item.x"></div>
-            </div>
-        </template>
-
-        <button @click="click">Click me</button>
-
+    <hr> <hr> <hr> <hr> <hr>
+    <div x-data>
+        <button id="target">Button</button>
+
+        <article
+            x-anchor.bottom
+            x-anchor:to="document.getElementById('target')"
+            :style="{ left: $anchor.x+'px', top: $anchor.y+'px' }"
+            style="position: absolute; left: 0;"
+        >
+            Tooltip contents
+        </article>
     </div>
-        <script>
-            document.addEventListener('alpine:init', () => {
-                Alpine.data('some_component', () => ({}));
-            });
-
-        </script>
-
-
-
-
-    <button wire:click.prefetch"...">
-        Do something
-    </button>
-
-
-
-
-
-
-
-
-
-
-<div x-data="{ count: $url(1) }">
-    <button @click="count++">+</button>
-    <button @click="count--">-</button>
-
-    <h1 x-text="count"></h1>
-</div>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-    <br>
-
-
-    <div x-data="{ users: [{ name: 'lebowski' }] }">
-        <template x-for="(user, idx) in users">
-            <span x-text="users[idx].name" x-yo></span>
-        </template>
-
-        <button @click="users = []">Reset</button>
-    </div>
-
-    <div x-data="{ foo: undefined }">
-        Yo: <input type="text" x-model="foo">
-    </div>
-
-    <!-- Play around here... -->
-
-                    <div class="relative">
-                        <div>Query: <span x-text="query"></span></div>
-                        <span class="inline-block w-full rounded-md shadow-sm">
-                            <div class="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left transition duration-150 ease-in-out focus-within:border-blue-700 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 sm:text-sm sm:leading-5">
-                                <span class="block flex flex-wrap gap-2">
-                                    <span x-show="activePersons.length === 0" class="p-0.5">Empty</span>
-                                    <template x-for="person in activePersons" :key="person.id">
-                                        <span class="flex items-center gap-1 rounded bg-blue-50 px-2 py-0.5">
-                                            <span x-text="person.name"></span>
-                                            <svg class="h-4 w-4 cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" @click.stop.prevent="removePerson(person)">
-                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
-                                            </svg>
-                                        </span>
-                                    </template>
-                                    <input x-combobox:input @change="query = $event.target.value" class="border-none p-0 focus:ring-0" placeholder="Search..." />
-                                </span>
-                                <button x-combobox:button class="absolute inset-y-0 right-0 flex items-center pr-2">
-                                    <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
-                                        <path d="M7 7l3-3 3 3m0 6l-3 3-3-3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
-                                    </svg>
-                                </button>
-                            </div>
-                        </span>
-
-                        <div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
-                            <ul x-combobox:options hold class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
-                                <template
-                                    x-for="person in people.filter((person) =>
-                                        person.name.toLowerCase().includes(query.toLowerCase())
-                                    )"
-                                    :key="person.id"
-                                >
-                                    <li x-combobox:option :value="person" class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none" :class="$comboboxOption.isActive ? 'bg-indigo-600 text-white' : 'text-gray-900'">
-                                        <span x-text="person.name" class="block truncate" :class="{ 'font-semibold': $comboboxOption.isSelected, 'font-normal': !$comboboxOption.isSelected }">
-                                        </span>
-                                        <span x-show="$comboboxOption.isSelected" class="absolute inset-y-0 right-0 flex items-center pr-4" :class="{ 'text-white': $comboboxOption.isActive, 'text-indigo-600': !$comboboxOption.isActive }">
-                                            <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
-                                                <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
-                                            </svg>
-                                        </span>
-                                    </li>
-                                </template>
-
-                                <!-- <template x-if="queryPerson">
-                                    <li x-combobox:option :value="queryPerson" class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none" :class="$comboboxOption.isActive ? 'bg-indigo-600 text-white' : 'text-gray-900'">
-                                        <span x-text="'Create ' + queryPerson.name" class="block truncate" :class="{ 'font-semibold': $comboboxOption.isSelected, 'font-normal': !$comboboxOption.isSelected }">
-                                        </span>
-                                        <span x-show="$comboboxOption.isSelected" class="absolute inset-y-0 right-0 flex items-center pr-4" :class="{ 'text-white': $comboboxOption.isActive, 'text-indigo-600': !$comboboxOption.isActive }">
-                                            <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
-                                                <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
-                                            </svg>
-                                        </span>
-                                    </li>
-                                </template> -->
-                            </ul>
-                        </div>
-                    </div>
-                </div>
-                <button class="mt-2 inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
-                    Submit
-                </button>
-            </form>
-        </div>
-    </div>
-</div>
-
-<div
-    x-data="{
-        query: '',
-        selected: null,
-        frameworks: [
-            {
-                id: 1,
-                name: 'Laravel',
-                disabled: false,
-            },
-            {
-                id: 2,
-                name: 'Ruby on Rails',
-                disabled: false,
-            },
-            {
-                id: 3,
-                name: 'Django',
-                disabled: false,
-            },
-            {
-                id: 4,
-                name: 'Express',
-                disabled: false,
-            },
-            {
-                id: 5,
-                name: 'Phoenix',
-                disabled: false,
-            },
-            {
-                id: 6,
-                name: 'Adonis',
-                disabled: false,
-            },
-            {
-                id: 7,
-                name: 'NextJS',
-                disabled: false,
-            },
-        ],
-        get filteredFrameworks() {
-            return this.query === ''
-                ? this.frameworks
-                : this.frameworks.filter((framework) => {
-                    return framework.name.toLowerCase().includes(this.query.toLowerCase())
-                })
-        }
-    }"
-
-    class="flex h-full w-screen justify-center bg-gray-50 p-12"
->
-    <div x-combobox x-model="selected">
-        <label x-combobox:label class="block text-sm text-gray-600">
-            Select framework
-        </label>
-
-        <div class="mt-1 relative">
-            <div class="flex items-center justify-between gap-2 w-64 bg-white pl-5 pr-3 py-2.5 rounded-md shadow">
-                <input
-                    x-combobox:input
-                    :display-value="framework => framework.name"
-                    @change="query = $event.target.value"
-                    class="border-none p-0 focus:outline-none focus:ring-0"
-                    placeholder="Search..."
-                />
-                <button x-combobox:button class="absolute inset-y-0 right-0 flex items-center pr-2">
-                    <!-- Heroicons up/down -->
-                    <svg class="shrink-0 w-5 h-5 text-gray-500" viewBox="0 0 20 20" fill="none" stroke="currentColor"><path d="M7 7l3-3 3 3m0 6l-3 3-3-3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
-                </button>
-            </div>
-
-            <div x-combobox:options x-cloak class="absolute right-0 w-64 max-h-60 mt-2 z-10 origin-top-right overflow-hidden bg-white border border-gray-200 rounded-md shadow-md outline-none" x-transition>
-                <ul class="divide-y divide-gray-100">
-                    <template
-                        x-for="framework in filteredFrameworks"
-                        :key="framework.id"
-                        hidden
-                    >
-                        <li
-                            x-combobox:option
-                            :value="framework"
-                            :disabled="framework.disabled"
-                            :class="{
-                                'bg-cyan-500/10 text-gray-900': $comboboxOption.isActive,
-                                'text-gray-600': ! $comboboxOption.isActive,
-                                'opacity-50 cursor-not-allowed': $comboboxOption.isDisabled,
-                            }"
-                            class="flex items-center cursor-default justify-between gap-2 w-full px-4 py-2 text-sm"
-                        >
-                            <span x-text="framework.name"></span>
-
-                            <span x-show="$comboboxOption.isSelected" class="text-cyan-600 font-bold">&check;</span>
-                        </li>
-                    </template>
-                </ul>
-
-                <p x-show="filteredFrameworks.length == 0" class="px-4 py-2 text-sm text-gray-600">No frameworks match your query.</p>
-            </div>
-        </div>
-        <div>local selected: <span x-text="selected?.name"></span></div>
-        <div>internal selected: <span x-text="$combobox.value?.name"></span></div>
-            <article x-text="$combobox.activeIndex"></article>
-    </div>
-</div>
-
-
-
-
+    <hr> <hr> <hr> <hr> <hr>
 </html>

+ 41 - 8
package-lock.json

@@ -7,6 +7,9 @@
             "workspaces": [
                 "packages/*"
             ],
+            "dependencies": {
+                "@floating-ui/dom": "^1.5.3"
+            },
             "devDependencies": {
                 "axios": "^0.21.1",
                 "chalk": "^4.1.1",
@@ -17,6 +20,10 @@
                 "jest": "^26.6.3"
             }
         },
+        "node_modules/@alpinejs/anchor": {
+            "resolved": "packages/anchor",
+            "link": true
+        },
         "node_modules/@alpinejs/collapse": {
             "resolved": "packages/collapse",
             "link": true
@@ -1014,6 +1021,28 @@
                 "node": ">=12"
             }
         },
+        "node_modules/@floating-ui/core": {
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
+            "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
+            "dependencies": {
+                "@floating-ui/utils": "^0.1.3"
+            }
+        },
+        "node_modules/@floating-ui/dom": {
+            "version": "1.5.3",
+            "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
+            "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
+            "dependencies": {
+                "@floating-ui/core": "^1.4.2",
+                "@floating-ui/utils": "^0.1.3"
+            }
+        },
+        "node_modules/@floating-ui/utils": {
+            "version": "0.1.6",
+            "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
+            "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
+        },
         "node_modules/@istanbuljs/load-nyc-config": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -7825,15 +7854,19 @@
             }
         },
         "packages/alpinejs": {
-            "version": "3.13.1",
+            "version": "3.13.2",
             "license": "MIT",
             "dependencies": {
                 "@vue/reactivity": "~3.1.1"
             }
         },
+        "packages/anchor": {
+            "version": "3.13.2",
+            "license": "MIT"
+        },
         "packages/collapse": {
             "name": "@alpinejs/collapse",
-            "version": "3.13.1",
+            "version": "3.13.2",
             "license": "MIT"
         },
         "packages/csp": {
@@ -7846,12 +7879,12 @@
         },
         "packages/docs": {
             "name": "@alpinejs/docs",
-            "version": "3.13.1-revision.1",
+            "version": "3.13.2-revision.1",
             "license": "MIT"
         },
         "packages/focus": {
             "name": "@alpinejs/focus",
-            "version": "3.13.1",
+            "version": "3.13.2",
             "license": "MIT",
             "dependencies": {
                 "focus-trap": "^6.9.4",
@@ -7868,17 +7901,17 @@
         },
         "packages/intersect": {
             "name": "@alpinejs/intersect",
-            "version": "3.13.1",
+            "version": "3.13.2",
             "license": "MIT"
         },
         "packages/mask": {
             "name": "@alpinejs/mask",
-            "version": "3.13.1",
+            "version": "3.13.2",
             "license": "MIT"
         },
         "packages/morph": {
             "name": "@alpinejs/morph",
-            "version": "3.13.1",
+            "version": "3.13.2",
             "license": "MIT"
         },
         "packages/navigate": {
@@ -7891,7 +7924,7 @@
         },
         "packages/persist": {
             "name": "@alpinejs/persist",
-            "version": "3.13.1",
+            "version": "3.13.2",
             "license": "MIT"
         },
         "packages/ui": {

+ 1 - 0
package.json

@@ -8,6 +8,7 @@
         "chalk": "^4.1.1",
         "cypress": "^7.0.0",
         "cypress-plugin-tab": "^1.0.5",
+        "@floating-ui/dom": "^1.5.3",
         "dot-json": "^1.2.2",
         "esbuild": "~0.16.17",
         "jest": "^26.6.3"

+ 1 - 0
packages/alpinejs/src/directives.js

@@ -203,6 +203,7 @@ let directiveOrder = [
     'ref',
     'data',
     'id',
+    'anchor',
     'bind',
     'init',
     'for',

+ 5 - 0
packages/anchor/builds/cdn.js

@@ -0,0 +1,5 @@
+import anchor from '../src/index.js'
+
+document.addEventListener('alpine:init', () => {
+    window.Alpine.plugin(anchor)
+})

+ 3 - 0
packages/anchor/builds/module.js

@@ -0,0 +1,3 @@
+import anchor from '../src/index.js'
+
+export default anchor

+ 17 - 0
packages/anchor/package.json

@@ -0,0 +1,17 @@
+{
+    "name": "@alpinejs/anchor",
+    "version": "3.13.2",
+    "description": "Anchor an element's position relative to another",
+    "homepage": "https://alpinejs.dev/plugins/anchor",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/anchor"
+    },
+    "author": "Caleb Porzio",
+    "license": "MIT",
+    "main": "dist/module.cjs.js",
+    "module": "dist/module.esm.js",
+    "unpkg": "dist/cdn.min.js",
+    "dependencies": {}
+}

+ 47 - 0
packages/anchor/src/index.js

@@ -0,0 +1,47 @@
+import { computePosition, autoUpdate, flip, offset, shift } from '@floating-ui/dom'
+
+export default function (Alpine) {
+    Alpine.magic('anchor', el => {
+        if (! el._x_anchor) throw 'Alpine: No x-anchor directive found on element using $anchor...'
+
+        return el._x_anchor
+    })
+
+    Alpine.directive('anchor', (el, { expression, modifiers, value }, { cleanup, evaluate }) => {
+        el._x_anchor = Alpine.reactive({ x: 0, y: 0 })
+
+        let reference = evaluate(expression)
+
+        if (! reference) throw 'Alpine: no element provided to x-anchor...'
+
+        let positions = ['top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end']
+        let placement = positions.find(i => modifiers.includes(i))
+
+        let offsetValue = 0
+
+        if (modifiers.includes('offset')) {
+            let idx = modifiers.findIndex(i => i === 'offset')
+
+            offsetValue = modifiers[idx + 1] !== undefined ? Number(modifiers[idx + 1]) : offsetValue
+        }
+
+        let release = autoUpdate(reference, el, () => {
+            let previousValue
+
+            computePosition(reference, el, {
+                placement,
+                middleware: [flip(), shift({padding: 5}), offset(offsetValue)],
+            }).then(({ x, y }) => {
+                // Only trigger Alpine reactivity when the value actually changes...
+                if (JSON.stringify({ x, y }) !== previousValue) {
+                    el._x_anchor.x = x
+                    el._x_anchor.y = y
+                }
+
+                previousValue = JSON.stringify({ x, y })
+            })
+        })
+
+        cleanup(() => release())
+    })
+}

+ 1 - 0
scripts/build.js

@@ -11,6 +11,7 @@ let zlib = require('zlib');
     'intersect',
     'persist',
     'collapse',
+    'anchor',
     'morph',
     'focus',
     'mask',