├── .github
├── FUNDING.yml
├── resources
│ ├── demo.mp4
│ ├── preview.png
│ └── pxlrbt-spotlight.png
└── workflows
│ └── code-style.yml
├── .gitignore
├── LICENSE.md
├── composer.json
├── mix-manifest.json
├── package-lock.json
├── package.json
├── readme.md
├── resources
├── css
│ └── spotlight.css
├── dist
│ ├── css
│ │ └── spotlight.css
│ └── js
│ │ └── spotlight.js
├── js
│ └── spotlight.js
└── lang
│ ├── de
│ └── spotlight.php
│ ├── en
│ └── spotlight.php
│ └── id
│ └── spotlight.php
├── src
├── Actions
│ ├── RegisterPages.php
│ ├── RegisterResources.php
│ └── RegisterUserMenu.php
├── Commands
│ ├── PageCommand.php
│ └── ResourceCommand.php
├── SpotlightPlugin.php
└── SpotlightServiceProvider.php
├── tailwind.config.js
└── webpack.mix.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: pxlrbt
2 |
--------------------------------------------------------------------------------
/.github/resources/demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pxlrbt/filament-spotlight/ddf57e3bb2a9a8a1a5076875134054f9f4c3beef/.github/resources/demo.mp4
--------------------------------------------------------------------------------
/.github/resources/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pxlrbt/filament-spotlight/ddf57e3bb2a9a8a1a5076875134054f9f4c3beef/.github/resources/preview.png
--------------------------------------------------------------------------------
/.github/resources/pxlrbt-spotlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pxlrbt/filament-spotlight/ddf57e3bb2a9a8a1a5076875134054f9f4c3beef/.github/resources/pxlrbt-spotlight.png
--------------------------------------------------------------------------------
/.github/workflows/code-style.yml:
--------------------------------------------------------------------------------
1 | name: Code Style
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | php-cs-fixer:
10 | name: Run Laravel Pint
11 | runs-on: ubuntu-latest
12 | permissions:
13 | contents: write
14 |
15 | steps:
16 | - name: Setup PHP
17 | uses: shivammathur/setup-php@v2
18 | with:
19 | php-version: '8.1'
20 |
21 | - name: Git checkout
22 | uses: actions/checkout@v3
23 | with:
24 | ref: ${{ github.head_ref }}
25 |
26 | - name: Install dependencies
27 | run: composer install -n --prefer-dist
28 |
29 | - name: Run Laravel Pint
30 | run: ./vendor/bin/pint
31 |
32 | - name: Commit changes
33 | uses: stefanzweifel/git-auto-commit-action@v4
34 | with:
35 | commit_message: Apply style changes
36 | file_pattern: '*.php'
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /node_modules
3 | composer.phar
4 | composer.lock
5 | .DS_Store
6 | .idea
7 | phpunit.xml
8 | .phpunit.result.cache
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2022] [Dennis Koch]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pxlrbt/filament-spotlight",
3 | "description": "Spotlight for Filament Admin",
4 | "license": "MIT",
5 | "keywords": [
6 | "laravel",
7 | "spotlight",
8 | "alfred",
9 | "wire-elements",
10 | "filament",
11 | "laravel-filament"
12 | ],
13 | "authors": [
14 | {
15 | "name": "Dennis Koch",
16 | "email": "info@pixelarbeit.de"
17 | }
18 | ],
19 | "require": {
20 | "php": "^8.0",
21 | "filament/filament": "^3.0.0-stable",
22 | "wire-elements/spotlight": "^2.0"
23 | },
24 | "require-dev": {
25 | "laravel/pint": "^1.10"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "pxlrbt\\FilamentSpotlight\\": "src/"
30 | }
31 | },
32 | "extra": {
33 | "laravel": {
34 | "providers": [
35 | "pxlrbt\\FilamentSpotlight\\SpotlightServiceProvider"
36 | ]
37 | }
38 | },
39 | "scripts": {
40 | "pint": "vendor/bin/pint"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/resources/dist/js/spotlight.js": "/resources/dist/js/spotlight.js",
3 | "/resources/dist/css/spotlight.css": "/resources/dist/css/spotlight.css"
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "mix watch",
5 | "build": "mix --production && npm run purge",
6 | "purge": "filament-purge -i resources/dist/css/spotlight.css -o resources/dist/css/spotlight.css -v 3.x"
7 | },
8 | "devDependencies": {
9 | "autoprefixer": "^10.1.0",
10 | "laravel-mix": "^6.0.6",
11 | "postcss": "^8.2.1",
12 | "tailwindcss": "^3.0.23",
13 | "@awcodes/filament-plugin-purge": "^1.0.1"
14 | },
15 | "dependencies": {
16 | "fuse.js": "^6.5.3"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 |
6 | # Filament Spotlight
7 |
8 |
9 |
10 | [](https://packagist.org/packages/pxlrbt/filament-spotlight)
11 | [](LICENSE.md)
12 | 
13 | [](https://packagist.org/packages/pxlrbt/filament-spotlight)
14 |
15 |
16 |
17 | Quickly navigate your Filament Resources with Spotlight functionality.
18 |
19 | Supports pages, resources and links from the user menu.
20 |
21 |
22 |
23 | https://user-images.githubusercontent.com/22632550/159757479-ca9c3f46-7638-4889-98ba-6164e5205509.mp4
24 |
25 |
26 |
27 |
28 | ## Installation
29 |
30 |
31 | | Plugin Version | Filament Version | PHP Version |
32 | |----------------|-----------------|-------------|
33 | | 0.x | 2.x | \> 8.0 |
34 | | 1.x | 3.x | \> 8.1 |
35 |
36 |
37 | ```bash
38 | composer require pxlrbt/filament-spotlight
39 | ```
40 |
41 | ### Assets
42 |
43 | Publish the assets (Filament > 3)
44 |
45 | ```bash
46 | php artisan filament:assets
47 | ```
48 |
49 | ### Plugin registration
50 |
51 | To use this plugin register it in your panel configuration:
52 |
53 | ```php
54 | use pxlrbt\FilamentSpotlight\SpotlightPlugin;
55 |
56 | $panel
57 | ->plugins([
58 | SpotlightPlugin::make(),
59 | ]);
60 | ```
61 |
62 | ## Usage
63 |
64 | There is no configuration needed.
65 |
66 | > "its genius"
67 |
68 | – Dan Harrin
69 |
70 | To open the Spotlight input bar you can use one of the following shortcuts:
71 |
72 | CTRL + K
73 | CMD + K
74 | CTRL + /
75 | CMD + /
76 |
77 | ### Setup
78 |
79 | This plugin relies on the same properties and methods used for Filament's global search. For records showing up with the correct name in "Edit/View" you need to set `$recordTitleAttribute`. [Check the docs for more information](https://filamentphp.com/docs/3.x/panels/resources/global-search)
80 |
81 | #### Excluding pages
82 |
83 | If you need to exclude a page from the spotlight results you may do so by adding a static `shouldRegisterSpotlight` method to the page and return false:
84 |
85 | ```php
86 | public static function shouldRegisterSpotlight(): bool
87 | {
88 | return false;
89 | }
90 | ```
91 |
92 | This can be useful when you have pages that require URL parameters.
93 |
94 | ## Translation
95 |
96 | To translate or edit the default placeholder, you have to publish the translation file for *wire-element/spotlight*:
97 |
98 | ```php
99 | php artisan vendor:publish --tag=livewire-ui-spotlight-translations
100 | ```
101 |
102 |
103 |
104 | ## Contributing
105 |
106 | If you want to contribute to this packages, you may want to test it in a real Filament project:
107 |
108 | - Fork this repository to your GitHub account.
109 | - Create a Filament app locally.
110 | - Clone your fork in your Filament app's root directory.
111 | - In the `/filament-spotlight` directory, create a branch for your fix, e.g. `fix/error-message`.
112 |
113 | Install the packages in your app's `composer.json`:
114 |
115 | ```json
116 | "require": {
117 | "pxlrbt/filament-spotlight": "dev-fix/error-message as main-dev",
118 | },
119 | "repositories": [
120 | {
121 | "type": "path",
122 | "url": "filament-spotlight"
123 | }
124 | ]
125 | ```
126 |
127 | Now, run `composer update`.
128 |
129 | ## Credits
130 | - [Dennis Koch](https://github.com/pxlrbt)
131 | - [All Contributors](../../contributors)
132 | - [Wire Elements Spotlight](https://github.com/wire-elements/spotlight)
133 |
--------------------------------------------------------------------------------
/resources/css/spotlight.css:
--------------------------------------------------------------------------------
1 | @tailwind components;
2 | @tailwind utilities;
3 |
4 | [x-cloak=""] {
5 | display: none !important;
6 | }
7 |
--------------------------------------------------------------------------------
/resources/dist/css/spotlight.css:
--------------------------------------------------------------------------------
1 | .right-5{right:1.25rem}.ml-1{margin-left:.25rem}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.pt-16{padding-top:4rem}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity))}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.duration-150,.transition-opacity{transition-duration:.15s}[x-cloak=""]{display:none!important}.hover\:bg-gray-800:hover{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.focus\:border-0:focus{border-width:0}.focus\:border-transparent:focus{border-color:transparent}.focus\:shadow-none:focus{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.sm\:pt-24{padding-top:6rem}}
--------------------------------------------------------------------------------
/resources/dist/js/spotlight.js:
--------------------------------------------------------------------------------
1 | (()=>{var e,t={794:(e,t,n)=>{n(27)},27:(e,t,n)=>{"use strict";function s(e){return Array.isArray?Array.isArray(e):"[object Array]"===d(e)}n.r(t);const i=1/0;function r(e){return null==e?"":function(e){if("string"==typeof e)return e;let t=e+"";return"0"==t&&1/e==-i?"-0":t}(e)}function c(e){return"string"==typeof e}function o(e){return"number"==typeof e}function h(e){return!0===e||!1===e||function(e){return a(e)&&null!==e}(e)&&"[object Boolean]"==d(e)}function a(e){return"object"==typeof e}function l(e){return null!=e}function u(e){return!e.trim().length}function d(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}const p=e=>`Missing ${e} property in key`,g=e=>`Property 'weight' in key '${e}' must be a positive integer`,f=Object.prototype.hasOwnProperty;class m{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach((e=>{let n=y(e);t+=n.weight,this._keys.push(n),this._keyMap[n.id]=n,t+=n.weight})),this._keys.forEach((e=>{e.weight/=t}))}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}}function y(e){let t=null,n=null,i=null,r=1,o=null;if(c(e)||s(e))i=e,t=M(e),n=x(e);else{if(!f.call(e,"name"))throw new Error(p("name"));const s=e.name;if(i=s,f.call(e,"weight")&&(r=e.weight,r<=0))throw new Error(g(s));t=M(s),n=x(s),o=e.getFn}return{path:t,id:n,weight:r,src:i,getFn:o}}function M(e){return s(e)?e:e.split(".")}function x(e){return s(e)?e.join("."):e}var v={isCaseSensitive:!1,includeScore:!1,keys:[],shouldSort:!0,sortFn:(e,t)=>e.score===t.score?e.idx{if(l(e))if(t[u]){const d=e[t[u]];if(!l(d))return;if(u===t.length-1&&(c(d)||o(d)||h(d)))n.push(r(d));else if(s(d)){i=!0;for(let e=0,n=d.length;e{this._keysMap[e.id]=t}))}create(){!this.isCreated&&this.docs.length&&(this.isCreated=!0,c(this.docs[0])?this.docs.forEach(((e,t)=>{this._addString(e,t)})):this.docs.forEach(((e,t)=>{this._addObject(e,t)})),this.norm.clear())}add(e){const t=this.size();c(e)?this._addString(e,t):this._addObject(e,t)}removeAt(e){this.records.splice(e,1);for(let t=e,n=this.size();t{let r=t.getFn?t.getFn(e):this.getFn(e,t.path);if(l(r))if(s(r)){let e=[];const t=[{nestedArrIndex:-1,value:r}];for(;t.length;){const{nestedArrIndex:n,value:i}=t.pop();if(l(i))if(c(i)&&!u(i)){let t={v:i,i:n,n:this.norm.get(i)};e.push(t)}else s(i)&&i.forEach(((e,n)=>{t.push({nestedArrIndex:n,value:e})}))}n.$[i]=e}else if(c(r)&&!u(r)){let e={v:r,n:this.norm.get(r)};n.$[i]=e}})),this.records.push(n)}toJSON(){return{keys:this.keys,records:this.records}}}function L(e,t,{getFn:n=v.getFn,fieldNormWeight:s=v.fieldNormWeight}={}){const i=new k({getFn:n,fieldNormWeight:s});return i.setKeys(e.map(y)),i.setSources(t),i.create(),i}function S(e,{errors:t=0,currentLocation:n=0,expectedLocation:s=0,distance:i=v.distance,ignoreLocation:r=v.ignoreLocation}={}){const c=t/e.length;if(r)return c;const o=Math.abs(s-n);return i?c+o/i:o?1:c}const I=32;function C(e,t,n,{location:s=v.location,distance:i=v.distance,threshold:r=v.threshold,findAllMatches:c=v.findAllMatches,minMatchCharLength:o=v.minMatchCharLength,includeMatches:h=v.includeMatches,ignoreLocation:a=v.ignoreLocation}={}){if(t.length>I)throw new Error(`Pattern length exceeds max of ${I}.`);const l=t.length,u=e.length,d=Math.max(0,Math.min(s,u));let p=r,g=d;const f=o>1||h,m=f?Array(u):[];let y;for(;(y=e.indexOf(t,g))>-1;){let e=S(t,{currentLocation:y,expectedLocation:d,distance:i,ignoreLocation:a});if(p=Math.min(e,p),g=y+l,f){let e=0;for(;e=h;r-=1){let c=r-1,o=n[e.charAt(c)];if(f&&(m[c]=+!!o),v[r]=(v[r+1]<<1|1)&o,s&&(v[r]|=(M[r+1]|M[r])<<1|1|M[r+1]),v[r]&k&&(x=S(t,{errors:s,currentLocation:c,expectedLocation:d,distance:i,ignoreLocation:a}),x<=p)){if(p=x,g=c,g<=d)break;h=Math.max(1,2*d-g)}}if(S(t,{errors:s+1,currentLocation:d,expectedLocation:d,distance:i,ignoreLocation:a})>p)break;M=v}const L={isMatch:g>=0,score:Math.max(.001,x)};if(f){const e=function(e=[],t=v.minMatchCharLength){let n=[],s=-1,i=-1,r=0;for(let c=e.length;r=t&&n.push([s,i]),s=-1)}return e[r-1]&&r-s>=t&&n.push([s,r-1]),n}(m,o);e.length?h&&(L.indices=e):L.isMatch=!1}return L}function _(e){let t={};for(let n=0,s=e.length;n{this.chunks.push({pattern:e,alphabet:_(e),startIndex:t})},l=this.pattern.length;if(l>I){let e=0;const t=l%I,n=l-t;for(;e{const{isMatch:g,score:f,indices:m}=C(e,t,d,{location:s+p,distance:i,threshold:r,findAllMatches:c,minMatchCharLength:o,includeMatches:n,ignoreLocation:h});g&&(u=!0),l+=f,g&&m&&(a=[...a,...m])}));let d={isMatch:u,score:u?l/this.chunks.length:1};return u&&n&&(d.indices=a),d}}class b{constructor(e){this.pattern=e}static isMultiMatch(e){return O(e,this.multiRegex)}static isSingleMatch(e){return O(e,this.singleRegex)}search(){}}function O(e,t){const n=e.match(t);return n?n[1]:null}class E extends b{constructor(e,{location:t=v.location,threshold:n=v.threshold,distance:s=v.distance,includeMatches:i=v.includeMatches,findAllMatches:r=v.findAllMatches,minMatchCharLength:c=v.minMatchCharLength,isCaseSensitive:o=v.isCaseSensitive,ignoreLocation:h=v.ignoreLocation}={}){super(e),this._bitapSearch=new $(e,{location:t,threshold:n,distance:s,includeMatches:i,findAllMatches:r,minMatchCharLength:c,isCaseSensitive:o,ignoreLocation:h})}static get type(){return"fuzzy"}static get multiRegex(){return/^"(.*)"$/}static get singleRegex(){return/^(.*)$/}search(e){return this._bitapSearch.searchIn(e)}}class A extends b{constructor(e){super(e)}static get type(){return"include"}static get multiRegex(){return/^'"(.*)"$/}static get singleRegex(){return/^'(.*)$/}search(e){let t,n=0;const s=[],i=this.pattern.length;for(;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,s.push([t,n-1]);const r=!!s.length;return{isMatch:r,score:r?0:1,indices:s}}}const R=[class extends b{constructor(e){super(e)}static get type(){return"exact"}static get multiRegex(){return/^="(.*)"$/}static get singleRegex(){return/^=(.*)$/}search(e){const t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}},A,class extends b{constructor(e){super(e)}static get type(){return"prefix-exact"}static get multiRegex(){return/^\^"(.*)"$/}static get singleRegex(){return/^\^(.*)$/}search(e){const t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}},class extends b{constructor(e){super(e)}static get type(){return"inverse-prefix-exact"}static get multiRegex(){return/^!\^"(.*)"$/}static get singleRegex(){return/^!\^(.*)$/}search(e){const t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}},class extends b{constructor(e){super(e)}static get type(){return"inverse-suffix-exact"}static get multiRegex(){return/^!"(.*)"\$$/}static get singleRegex(){return/^!(.*)\$$/}search(e){const t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}},class extends b{constructor(e){super(e)}static get type(){return"suffix-exact"}static get multiRegex(){return/^"(.*)"\$$/}static get singleRegex(){return/^(.*)\$$/}search(e){const t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}},class extends b{constructor(e){super(e)}static get type(){return"inverse-exact"}static get multiRegex(){return/^!"(.*)"$/}static get singleRegex(){return/^!(.*)$/}search(e){const t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}},E],F=R.length,N=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;const D=new Set([E.type,A.type]);class j{constructor(e,{isCaseSensitive:t=v.isCaseSensitive,includeMatches:n=v.includeMatches,minMatchCharLength:s=v.minMatchCharLength,ignoreLocation:i=v.ignoreLocation,findAllMatches:r=v.findAllMatches,location:c=v.location,threshold:o=v.threshold,distance:h=v.distance}={}){this.query=null,this.options={isCaseSensitive:t,includeMatches:n,minMatchCharLength:s,findAllMatches:r,ignoreLocation:i,location:c,threshold:o,distance:h},this.pattern=t?e:e.toLowerCase(),this.query=function(e,t={}){return e.split("|").map((e=>{let n=e.trim().split(N).filter((e=>e&&!!e.trim())),s=[];for(let e=0,i=n.length;e!(!e[q]&&!e[T]),Q=e=>({[q]:Object.keys(e).map((t=>({[t]:e[t]})))});function V(e,t,{auto:n=!0}={}){const i=e=>{let r=Object.keys(e);const o=(e=>!!e[z])(e);if(!o&&r.length>1&&!K(e))return i(Q(e));if((e=>!s(e)&&a(e)&&!K(e))(e)){const s=o?e[z]:r[0],i=o?e[J]:e[s];if(!c(i))throw new Error((e=>`Invalid value for key ${e}`)(s));const h={keyId:x(s),pattern:i};return n&&(h.searcher=P(i,t)),h}let h={children:[],operator:r[0]};return r.forEach((t=>{const n=e[t];s(n)&&n.forEach((e=>{h.children.push(i(e))}))})),h};return K(e)||(e=Q(e)),i(e)}function U(e,t){const n=e.matches;t.matches=[],l(n)&&n.forEach((e=>{if(!l(e.indices)||!e.indices.length)return;const{indices:n,value:s}=e;let i={indices:n,value:s};e.key&&(i.key=e.key.src),e.idx>-1&&(i.refIndex=e.idx),t.matches.push(i)}))}function B(e,t){t.score=e.score}class G{constructor(e,t={},n){this.options={...v,...t},this.options.useExtendedSearch,this._keyStore=new m(this.options.keys),this.setCollection(e,n)}setCollection(e,t){if(this._docs=e,t&&!(t instanceof k))throw new Error("Incorrect 'index' type");this._myIndex=t||L(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}add(e){l(e)&&(this._docs.push(e),this._myIndex.add(e))}remove(e=(()=>!1)){const t=[];for(let n=0,s=this._docs.length;n{let n=1;e.matches.forEach((({key:e,norm:s,score:i})=>{const r=e?e.weight:null;n*=Math.pow(0===i&&r?Number.EPSILON:i,(r||1)*(t?1:s))})),e.score=n}))}(a,{ignoreFieldNorm:h}),i&&a.sort(r),o(t)&&t>-1&&(a=a.slice(0,t)),function(e,t,{includeMatches:n=v.includeMatches,includeScore:s=v.includeScore}={}){const i=[];return n&&i.push(U),s&&i.push(B),e.map((e=>{const{idx:n}=e,s={item:t[n],refIndex:n};return i.length&&i.forEach((t=>{t(e,s)})),s}))}(a,this._docs,{includeMatches:n,includeScore:s})}_searchStringList(e){const t=P(e,this.options),{records:n}=this._myIndex,s=[];return n.forEach((({v:e,i:n,n:i})=>{if(!l(e))return;const{isMatch:r,score:c,indices:o}=t.searchIn(e);r&&s.push({item:e,idx:n,matches:[{score:c,value:e,norm:i,indices:o}]})})),s}_searchLogical(e){const t=V(e,this.options),n=(e,t,s)=>{if(!e.children){const{keyId:n,searcher:i}=e,r=this._findMatches({key:this._keyStore.get(n),value:this._myIndex.getValueForItemAtKeyId(t,n),searcher:i});return r&&r.length?[{idx:s,item:t,matches:r}]:[]}const i=[];for(let r=0,c=e.children.length;r{if(l(e)){let c=n(t,e,s);c.length&&(i[s]||(i[s]={idx:s,item:e,matches:[]},r.push(i[s])),c.forEach((({matches:e})=>{i[s].matches.push(...e)})))}})),r}_searchObjectList(e){const t=P(e,this.options),{keys:n,records:s}=this._myIndex,i=[];return s.forEach((({$:e,i:s})=>{if(!l(e))return;let r=[];n.forEach(((n,s)=>{r.push(...this._findMatches({key:n,value:e[s],searcher:t}))})),r.length&&i.push({idx:s,item:e,matches:r})})),i}_findMatches({key:e,value:t,searcher:n}){if(!l(t))return[];let i=[];if(s(t))t.forEach((({v:t,i:s,n:r})=>{if(!l(t))return;const{isMatch:c,score:o,indices:h}=n.searchIn(t);c&&i.push({score:o,key:e,value:t,idx:s,norm:r,indices:h})}));else{const{v:s,n:r}=t,{isMatch:c,score:o,indices:h}=n.searchIn(s);c&&i.push({score:o,key:e,value:s,norm:r,indices:h})}return i}}G.version="6.6.2",G.createIndex=L,G.parseIndex=function(e,{getFn:t=v.getFn,fieldNormWeight:n=v.fieldNormWeight}={}){const{keys:s,records:i}=e,r=new k({getFn:t,fieldNormWeight:n});return r.setKeys(s),r.setIndexRecords(i),r},G.config=v,G.parseQuery=V,function(...e){W.push(...e)}(j),window.LivewireUISpotlight=function(e){return{inputPlaceholder:e.placeholder,searchEngine:"commands",commands:e.commands,commandSearch:null,selectedCommand:null,dependencySearch:null,dependencyQueryResults:window.Livewire.find(e.componentId).entangle("dependencyQueryResults"),requiredDependencies:[],currentDependency:null,resolvedDependencies:{},showResultsWithoutInput:e.showResultsWithoutInput,init:function(){var t=this;this.commandSearch=new G(this.commands,{threshold:.3,keys:["name","description","synonyms"]}),this.dependencySearch=new G([],{threshold:.3,keys:["name","description","synonyms"]}),this.$watch("dependencyQueryResults",(function(e){t.dependencySearch.setCollection(e)})),this.$watch("input",(function(e){0===e.length&&(t.selected=0),null!==t.selectedCommand&&null!==t.currentDependency&&"search"===t.currentDependency.type&&t.$wire.searchDependency(t.selectedCommand.id,t.currentDependency.id,e,t.resolvedDependencies)})),this.$watch("isOpen",(function(n){!1===n&&setTimeout((function(){t.input="",t.inputPlaceholder=e.placeholder,t.searchEngine="commands",t.resolvedDependencies={},t.selectedCommand=null,t.currentDependency=null,t.selectedCommand=null,t.requiredDependencies=[]}),300)}))},isOpen:!1,toggleOpen:function(){var e=this;this.isOpen?this.isOpen=!1:(this.input="",this.isOpen=!0,setTimeout((function(){e.$refs.input.focus()}),100))},input:"",filteredItems:function(){return"commands"===this.searchEngine?!this.input&&this.showResultsWithoutInput?this.commandSearch.getIndex().docs.map((function(e,t){return[{item:e},t]})):this.commandSearch.search(this.input).map((function(e,t){return[e,t]})):"search"===this.searchEngine?!this.input&&this.showResultsWithoutInput?this.dependencySearch.getIndex().docs.map((function(e,t){return[{item:e},t]})):this.dependencySearch.search(this.input).map((function(e,t){return[e,t]})):[]},selectUp:function(){var e=this;this.selected=Math.max(0,this.selected-1),this.$nextTick((function(){e.$refs.results.children[e.selected+1].scrollIntoView({block:"nearest"})}))},selectDown:function(){var e=this;this.selected=Math.min(this.filteredItems().length-1,this.selected+1),this.$nextTick((function(){e.$refs.results.children[e.selected+1].scrollIntoView({block:"nearest"})}))},go:function(e){var t,n=this;(null===this.selectedCommand&&(this.selectedCommand=this.commands.find((function(t){return t.id===(e||n.filteredItems()[n.selected][0].item.id)})),this.requiredDependencies=JSON.parse(JSON.stringify(this.selectedCommand.dependencies))),null!==this.currentDependency)&&(t="search"===this.currentDependency.type?e||this.filteredItems()[this.selected][0].item.id:this.input,this.resolvedDependencies[this.currentDependency.id]=t);this.requiredDependencies.length>0?(this.input="",this.currentDependency=this.requiredDependencies.pop(),this.inputPlaceholder=this.currentDependency.placeholder,this.searchEngine="search"===this.currentDependency.type&&"search"):(this.isOpen=!1,this.$wire.execute(this.selectedCommand.id,this.resolvedDependencies))},selected:0}}},578:()=>{}},n={};function s(e){var i=n[e];if(void 0!==i)return i.exports;var r=n[e]={exports:{}};return t[e](r,r.exports,s),r.exports}s.m=t,e=[],s.O=(t,n,i,r)=>{if(!n){var c=1/0;for(l=0;l=r)&&Object.keys(s.O).every((e=>s.O[e](n[h])))?n.splice(h--,1):(o=!1,r0&&e[l-1][2]>r;l--)e[l]=e[l-1];e[l]=[n,i,r]},s.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),s.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e={845:0,410:0};s.O.j=t=>0===e[t];var t=(t,n)=>{var i,r,[c,o,h]=n,a=0;if(c.some((t=>0!==e[t]))){for(i in o)s.o(o,i)&&(s.m[i]=o[i]);if(h)var l=h(s)}for(t&&t(n);as(794)));var i=s.O(void 0,[410],(()=>s(578)));i=s.O(i)})();
--------------------------------------------------------------------------------
/resources/js/spotlight.js:
--------------------------------------------------------------------------------
1 | require('/vendor/wire-elements/spotlight/resources/js/spotlight.js');
2 |
--------------------------------------------------------------------------------
/resources/lang/de/spotlight.php:
--------------------------------------------------------------------------------
1 | 'Nach :Record suchen',
5 | 'account' => 'Dein Konto',
6 | ];
7 |
--------------------------------------------------------------------------------
/resources/lang/en/spotlight.php:
--------------------------------------------------------------------------------
1 | 'Search for a :record',
5 | 'account' => 'Your account',
6 | ];
7 |
--------------------------------------------------------------------------------
/resources/lang/id/spotlight.php:
--------------------------------------------------------------------------------
1 | 'Cari :record',
5 | 'account' => 'Akun Anda',
6 | ];
7 |
--------------------------------------------------------------------------------
/src/Actions/RegisterPages.php:
--------------------------------------------------------------------------------
1 | getPages();
15 |
16 | foreach ($pages as $pageClass) {
17 |
18 | /**
19 | * @var Page $page
20 | */
21 | $page = new $pageClass;
22 |
23 | if (self::hasParameters($page::getSlug())) {
24 | continue;
25 | }
26 |
27 | if (method_exists($page, 'shouldRegisterSpotlight') && $page::shouldRegisterSpotlight() === false) {
28 | continue;
29 | }
30 |
31 | $name = collect([
32 | $page->getNavigationGroup(),
33 | $page->getTitle(),
34 | ])->filter()->join(' / ');
35 |
36 | $url = $page::getUrl();
37 |
38 | if (blank($name) || blank($url)) {
39 | continue;
40 | }
41 |
42 | $command = new PageCommand(
43 | name: $name,
44 | url: $url
45 | );
46 |
47 | Spotlight::$commands[$command->getId()] = $command;
48 | }
49 | }
50 |
51 | private static function hasParameters(string $slug): bool
52 | {
53 | return preg_match('/{[^}]+}/', $slug) === 1;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Actions/RegisterResources.php:
--------------------------------------------------------------------------------
1 | getResources();
15 |
16 | foreach ($resources as $resource) {
17 | if (method_exists($resource, 'shouldRegisterSpotlight') && $resource::shouldRegisterSpotlight() === false) {
18 | continue;
19 | }
20 |
21 | $pages = $resource::getPages();
22 |
23 | foreach ($pages as $key => $page) {
24 | if (method_exists($page->getPage(), 'shouldRegisterSpotlight') && $page->getPage()::shouldRegisterSpotlight() === false) {
25 | continue;
26 | }
27 |
28 | /**
29 | * @var PageRegistration $page
30 | */
31 | if (blank($key) || blank($page->getPage())) {
32 | continue;
33 | }
34 |
35 | $command = new ResourceCommand(
36 | resource: $resource,
37 | page: $page->getPage(),
38 | key: $key,
39 | );
40 |
41 | Spotlight::$commands[$command->getId()] = $command;
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Actions/RegisterUserMenu.php:
--------------------------------------------------------------------------------
1 | $items
18 | */
19 | $items = $panel->getUserMenuItems();
20 |
21 | foreach ($items as $key => $item) {
22 | $name = $self->getName($key, $item);
23 | $url = $self->getUrl($key, $item);
24 |
25 | if (blank($name) || blank($url)) {
26 | continue;
27 | }
28 |
29 | $command = new PageCommand(
30 | name: $name,
31 | url: $url,
32 | );
33 |
34 | Spotlight::$commands[$command->getId()] = $command;
35 | }
36 | }
37 |
38 | protected function getName(string $key, MenuItem $item): ?string
39 | {
40 | return match ($key) {
41 | 'account' => $item->getLabel() ?? __('filament-spotlight::spotlight.account'),
42 | 'logout' => $item->getLabel() ?? __('filament-panels::layout.actions.logout.label'),
43 | default => $item->getLabel()
44 | };
45 | }
46 |
47 | protected function getUrl(string $key, MenuItem $item): ?string
48 | {
49 | return match ($key) {
50 | 'logout' => $item->getUrl() ?? Filament::getLogoutUrl(),
51 | default => $item->getUrl()
52 | };
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Commands/PageCommand.php:
--------------------------------------------------------------------------------
1 | url);
21 | }
22 |
23 | public function shouldBeShown(): bool
24 | {
25 | return $this->shouldBeShown;
26 | }
27 |
28 | public function execute(Spotlight $spotlight): void
29 | {
30 | $spotlight->redirect($this->url);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Commands/ResourceCommand.php:
--------------------------------------------------------------------------------
1 | $resource
33 | * @param class-string $page
34 | */
35 | public function __construct(
36 | string $resource,
37 | string $page,
38 | protected string $key,
39 | ) {
40 | $this->resource = new $resource;
41 | $this->page = new $page;
42 | }
43 |
44 | public function getId(): string
45 | {
46 | return md5($this->resource::class.$this->page::class);
47 | }
48 |
49 | public function getName(): string
50 | {
51 | return collect([
52 | $this->resource::getNavigationGroup(),
53 | $this->resource::getBreadcrumb(),
54 | $this->page::getNavigationLabel(),
55 | ])
56 | ->filter()
57 | ->join(' / ');
58 | }
59 |
60 | public function getUrl(null|int|string $recordKey): string
61 | {
62 | return $this->resource::getUrl($this->key, $recordKey ? ['record' => $recordKey] : []);
63 | }
64 |
65 | public function shouldBeShown(): bool
66 | {
67 | return match (true) {
68 | $this->page instanceof CreateRecord => $this->resource::canCreate(),
69 | default => $this->resource::canViewAny(),
70 | };
71 | }
72 |
73 | protected function hasDependencies(): bool
74 | {
75 | return match (true) {
76 | $this->page instanceof EditRecord => true,
77 | $this->page instanceof ViewRecord => true,
78 | $this->page instanceof ManageRelatedRecords => true,
79 | default => false,
80 | };
81 | }
82 |
83 | public function dependencies(): ?SpotlightCommandDependencies
84 | {
85 | if (! $this->hasDependencies()) {
86 | return null;
87 | }
88 |
89 | return SpotlightCommandDependencies::collection()->add(
90 | SpotlightCommandDependency::make('record')->setPlaceholder(
91 | __('filament-spotlight::spotlight.placeholder', ['record' => $this->resource::getModelLabel()])
92 | )
93 | );
94 | }
95 |
96 | public function searchRecord($query): EloquentCollection|Collection|array
97 | {
98 | $resource = $this->resource;
99 | $searchQuery = $query;
100 | $query = $resource::getGlobalSearchEloquentQuery();
101 |
102 | foreach (explode(' ', $searchQuery) as $searchQueryWord) {
103 | $query->where(function (Builder $query) use ($searchQueryWord, $resource) {
104 | $isFirst = true;
105 |
106 | foreach ($resource::getGloballySearchableAttributes() as $attributes) {
107 | static::applyGlobalSearchAttributeConstraint($query, Arr::wrap($attributes), $searchQueryWord, $isFirst, $resource);
108 | }
109 | });
110 | }
111 |
112 | return $query
113 | ->limit(50)
114 | ->get()
115 | ->map(fn (Model $record) => new SpotlightSearchResult(
116 | $record->getRouteKey(),
117 | $resource::getGlobalSearchResultTitle($record),
118 | collect($resource::getGlobalSearchResultDetails($record))
119 | ->map(fn ($value, $key) => $key.': '.$value)
120 | ->join(' – ')
121 | ));
122 | }
123 |
124 | protected static function applyGlobalSearchAttributeConstraint(Builder $query, array $searchAttributes, string $searchQuery, bool &$isFirst, $resource): Builder
125 | {
126 | $isForcedCaseInsensitive = $resource::isGlobalSearchForcedCaseInsensitive();
127 |
128 | /** @var Connection $databaseConnection */
129 | $databaseConnection = $query->getConnection();
130 |
131 | if ($isForcedCaseInsensitive) {
132 | $searchQuery = strtolower($searchQuery);
133 | }
134 |
135 | foreach ($searchAttributes as $searchAttribute) {
136 | $whereClause = $isFirst ? 'where' : 'orWhere';
137 |
138 | $query->when(
139 | str($searchAttribute)->contains('.'),
140 | function (Builder $query) use ($databaseConnection, $isForcedCaseInsensitive, $searchAttribute, $searchQuery, $whereClause): Builder {
141 | return $query->{"{$whereClause}Relation"}(
142 | (string) str($searchAttribute)->beforeLast('.'),
143 | generate_search_column_expression((string) str($searchAttribute)->afterLast('.'), $isForcedCaseInsensitive, $databaseConnection),
144 | 'like',
145 | "%{$searchQuery}%",
146 | );
147 | },
148 | fn (Builder $query) => $query->{$whereClause}(
149 | generate_search_column_expression($searchAttribute, $isForcedCaseInsensitive, $databaseConnection),
150 | 'like',
151 | "%{$searchQuery}%",
152 | ),
153 | );
154 |
155 | $isFirst = false;
156 | }
157 |
158 | return $query;
159 | }
160 |
161 | public function execute(Spotlight $spotlight, $record = null): void
162 | {
163 | $spotlight->redirect($this->getUrl($record));
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/SpotlightPlugin.php:
--------------------------------------------------------------------------------
1 | renderHook(
32 | 'panels::scripts.after',
33 | fn () => Blade::render("@livewire('livewire-ui-spotlight')")
34 | );
35 | }
36 |
37 | public function boot(Panel $panel): void
38 | {
39 | Filament::serving(function () use ($panel) {
40 | config()->set('livewire-ui-spotlight.include_js', false);
41 |
42 | if (Filament::hasTenancy()) {
43 | Event::listen(TenantSet::class, function () use ($panel) {
44 | self::registerNavigation($panel);
45 | });
46 | } else {
47 | self::registerNavigation($panel);
48 | }
49 |
50 | });
51 |
52 | }
53 |
54 | public static function registerNavigation($panel)
55 | {
56 | RegisterPages::boot($panel);
57 | RegisterResources::boot($panel);
58 | RegisterUserMenu::boot($panel);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/SpotlightServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadTranslationsFrom(__DIR__.'/../resources/lang', 'filament-spotlight');
15 |
16 | config()->set('livewire-ui-spotlight.commands', []);
17 |
18 | FilamentAsset::register([
19 | Css::make('spotlight-css', __DIR__.'/../resources/dist/css/spotlight.css'),
20 | Js::make('spotlight-js', __DIR__.'/../resources/dist/js/spotlight.js'),
21 | ], package: 'pxlrbt/filament-spotlight');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | './vendor/wire-elements/**/*.blade.php',
4 | ],
5 | };
6 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | const mix = require('laravel-mix');
2 |
3 | /*
4 | |--------------------------------------------------------------------------
5 | | Mix Asset Management
6 | |--------------------------------------------------------------------------
7 | |
8 | | Mix provides a clean, fluent API for defining some Webpack build steps
9 | | for your Laravel applications. By default, we are compiling the CSS
10 | | file for the application as well as bundling up all the JS files.
11 | |
12 | */
13 |
14 | mix
15 | .js('resources/js/spotlight.js', 'resources/dist/js')
16 | .postCss('resources/css/spotlight.css', 'resources/dist/css', [
17 | require('tailwindcss'),
18 | require('autoprefixer'),
19 | ]);
20 |
--------------------------------------------------------------------------------