├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── dist ├── css │ └── filter.css ├── js │ └── filter.js └── mix-manifest.json ├── nova.mix.js ├── package.json ├── resources ├── css │ └── filter.css └── js │ ├── components │ └── Filter.vue │ └── filter.js ├── screenshots ├── dark.png └── light.png ├── src ├── FilterServiceProvider.php └── SliderFilter.php ├── tailwind.config.js ├── webpack.mix.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: milewski 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | package-lock.json 5 | composer.phar 6 | composer.lock 7 | phpunit.xml 8 | .phpunit.result.cache 9 | .DS_Store 10 | Thumbs.db 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Digital Creative 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nova Slider Filter 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/digital-creative/nova-slider-filter)](https://packagist.org/packages/digital-creative/nova-slider-filter) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/digital-creative/nova-slider-filter)](https://packagist.org/packages/digital-creative/nova-slider-filter) 5 | [![License](https://img.shields.io/packagist/l/digital-creative/nova-slider-filter)](https://github.com/dcasia/nova-slider-filter/blob/main/LICENSE) 6 | 7 | A Laravel Nova filter for picking range between a min/max value. 8 | 9 | 10 | 11 | SliderFilter in Action 12 | 13 | 14 | # Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```shell 19 | composer require digital-creative/nova-slider-filter 20 | ``` 21 | 22 | ## Basic Usage 23 | 24 | Create a filter as usual and extend the `DigitalCreative\SliderFilter\SliderFilter` class 25 | 26 | ```php 27 | use DigitalCreative\SliderFilter\SliderFilter; 28 | 29 | class MyFilter extends SliderFilter { 30 | 31 | public function apply(NovaRequest $request, $query, $values) 32 | { 33 | // $values will be an array when using ->range() and int when using ->single() 34 | } 35 | 36 | } 37 | ``` 38 | 39 | and use it as usual on the filters methods within your resource class: 40 | 41 | ```php 42 | class ExampleNovaResource extends Resource { 43 | 44 | public function filters(NovaRequest $request): array 45 | { 46 | return [ 47 | MyFilter::make()->single(min: 0, max: 100), 48 | ]; 49 | } 50 | 51 | } 52 | ``` 53 | 54 | Calling the `->range()` method will render a slider with two (or more) handles, while calling the `single()` method will render a slider with a single handle. 55 | 56 | ```php 57 | class ExampleNovaResource extends Resource { 58 | 59 | public function filters(NovaRequest $request): array 60 | { 61 | return [ 62 | MyFilter::make() 63 | ->range(0, 500, 300) 64 | ->label('${value}') 65 | 66 | MyFilter::make() 67 | ->single(min: 0, max: 500) 68 | ->label('${value}') 69 | ]; 70 | } 71 | 72 | } 73 | ``` 74 | 75 | You can also set marks on the slider by using the `->marks()` method. The method accepts an array of key/value pairs where the key is the value of the mark and the value is the label to be displayed. 76 | 77 | ```php 78 | class ExampleNovaResource extends Resource { 79 | 80 | public function filters(NovaRequest $request): array 81 | { 82 | return [ 83 | MyFilter::make() 84 | ->single(0, 100) 85 | ->marks([ 86 | '0' => '🌑', 87 | '50' => '🌓', 88 | '100' => '🌕' 89 | ]) 90 | ]; 91 | } 92 | 93 | } 94 | ``` 95 | 96 | ## ⭐️ Show Your Support 97 | 98 | Please give a ⭐️ if this project helped you! 99 | 100 | ### Other Packages You Might Like 101 | 102 | - [Nova Dashboard](https://github.com/dcasia/nova-dashboard) - The missing dashboard for Laravel Nova! 103 | - [Nova Welcome Card](https://github.com/dcasia/nova-welcome-card) - A configurable version of the `Help card` that comes with Nova. 104 | - [Icon Action Toolbar](https://github.com/dcasia/icon-action-toolbar) - Replaces the default boring action menu with an inline row of icon-based actions. 105 | - [Expandable Table Row](https://github.com/dcasia/expandable-table-row) - Provides an easy way to append extra data to each row of your resource tables. 106 | - [Collapsible Resource Manager](https://github.com/dcasia/collapsible-resource-manager) - Provides an easy way to order and group your resources on the sidebar. 107 | - [Resource Navigation Tab](https://github.com/dcasia/resource-navigation-tab) - Organize your resource fields into tabs. 108 | - [Resource Navigation Link](https://github.com/dcasia/resource-navigation-link) - Create links to internal or external resources. 109 | - [Nova Mega Filter](https://github.com/dcasia/nova-mega-filter) - Display all your filters in a card instead of a tiny dropdown! 110 | - [Nova Pill Filter](https://github.com/dcasia/nova-pill-filter) - A Laravel Nova filter that renders into clickable pills. 111 | - [Nova Slider Filter](https://github.com/dcasia/nova-slider-filter) - A Laravel Nova filter for picking range between a min/max value. 112 | - [Nova Range Input Filter](https://github.com/dcasia/nova-range-input-filter) - A Laravel Nova range input filter. 113 | - [Nova FilePond](https://github.com/dcasia/nova-filepond) - A Nova field for uploading File, Image and Video using Filepond. 114 | - [Custom Relationship Field](https://github.com/dcasia/custom-relationship-field) - Emulate HasMany relationship without having a real relationship set between resources. 115 | - [Column Toggler](https://github.com/dcasia/column-toggler) - A Laravel Nova package that allows you to hide/show columns in the index view. 116 | - [Batch Edit Toolbar](https://github.com/dcasia/batch-edit-toolbar) - Allows you to update a single column of a resource all at once directly from the index page. 117 | 118 | ## License 119 | 120 | The MIT License (MIT). Please see [License File](https://raw.githubusercontent.com/dcasia/nova-slider-filter/master/LICENSE) for more information. 121 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digital-creative/nova-slider-filter", 3 | "description": "A Laravel Nova filter for picking range between a min/max value.", 4 | "keywords": [ 5 | "laravel", 6 | "nova", 7 | "slider", 8 | "filter", 9 | "range" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Rafael Milewski" 14 | } 15 | ], 16 | "license": "MIT", 17 | "require": { 18 | "php": ">=8.0", 19 | "laravel/nova": "^4.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "DigitalCreative\\NovaRangeFilter\\": "src/" 24 | } 25 | }, 26 | "extra": { 27 | "laravel": { 28 | "providers": [ 29 | "DigitalCreative\\NovaRangeFilter\\FilterServiceProvider" 30 | ] 31 | } 32 | }, 33 | "config": { 34 | "sort-packages": true 35 | }, 36 | "minimum-stability": "dev", 37 | "prefer-stable": true 38 | } 39 | -------------------------------------------------------------------------------- /dist/css/filter.css: -------------------------------------------------------------------------------- 1 | .nova-slider-filter :is(.ml-2){margin-left:.5rem}.nova-slider-filter :is(.mr-2){margin-right:.5rem}.nova-slider-filter :is(.flex){display:flex}.nova-slider-filter :is(.flex-shrink-0){flex-shrink:0}.nova-slider-filter :is(.flex-row){flex-direction:row}.nova-slider-filter :is(.items-center){align-items:center}.nova-slider-filter :is(.justify-center){justify-content:center}.nova-slider-filter :is(.p-2){padding:.5rem}.nova-slider-filter :is(.pb-6){padding-bottom:1.5rem}.nova-slider-filter :is(.pt-6){padding-top:1.5rem}.nova-slider-filter :is(.text-xs){font-size:.75rem;line-height:1rem}.nova-slider-filter :is(.\!filter){filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.nova-slider-filter :is(.filter){filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)} 2 | -------------------------------------------------------------------------------- /dist/js/filter.js: -------------------------------------------------------------------------------- 1 | (()=>{var t,e={514:(t,e,r)=>{"use strict";var n=r(311);var i=r(454);function o(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=r){var n,i,o,a,s=[],l=!0,u=!1;try{if(o=(r=r.call(t)).next,0===e){if(Object(r)!==r)return;l=!1}else for(;!(l=(n=o.call(r)).done)&&(s.push(n.value),s.length!==e);l=!0);}catch(t){u=!0,i=t}finally{try{if(!l&&null!=r.return&&(a=r.return(),Object(a)!==a))return}finally{if(u)throw i}}return s}}(t,e)||s(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function a(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||s(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(t,e){if(t){if("string"==typeof t)return l(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?l(t,e):void 0}}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r{"use strict";r.d(e,{Z:()=>o});var n=r(645),i=r.n(n)()((function(t){return t[1]}));i.push([t.id,'.vue-slider-disabled{cursor:not-allowed;opacity:.5}.vue-slider-rail{background-color:#ccc;border-radius:15px}.vue-slider-process{background-color:#3498db;border-radius:15px}.vue-slider-mark{z-index:4}.vue-slider-mark:first-child .vue-slider-mark-step,.vue-slider-mark:last-child .vue-slider-mark-step{display:none}.vue-slider-mark-step{background-color:rgba(0,0,0,.16);border-radius:50%;height:100%;width:100%}.vue-slider-mark-label{font-size:14px;white-space:nowrap}.vue-slider-dot-handle{background-color:#fff;border-radius:50%;box-shadow:.5px .5px 2px 1px rgba(0,0,0,.32);box-sizing:border-box;cursor:pointer;height:100%;width:100%}.vue-slider-dot-handle-focus{box-shadow:0 0 1px 2px rgba(52,152,219,.36)}.vue-slider-dot-handle-disabled{background-color:#ccc;cursor:not-allowed}.vue-slider-dot-tooltip-inner{background-color:#3498db;border-color:#3498db;border-radius:5px;box-sizing:content-box;color:#fff;font-size:14px;min-width:20px;padding:2px 5px;text-align:center;white-space:nowrap}.vue-slider-dot-tooltip-inner:after{content:"";position:absolute}.vue-slider-dot-tooltip-inner-top:after{border:0 solid transparent;border-top-color:inherit;height:0;left:50%;top:100%;transform:translate(-50%);width:0}.vue-slider-dot-tooltip-inner-bottom:after{border:0 solid transparent;border-bottom-color:inherit;bottom:100%;height:0;left:50%;transform:translate(-50%);width:0}.vue-slider-dot-tooltip-inner-left:after{border:0 solid transparent;border-left-color:inherit;height:0;left:100%;top:50%;transform:translateY(-50%);width:0}.vue-slider-dot-tooltip-inner-right:after{border:0 solid transparent;border-right-color:inherit;height:0;right:100%;top:50%;transform:translateY(-50%);width:0}.vue-slider-dot-tooltip-wrapper{opacity:0;transition:all .3s}.vue-slider-dot-tooltip-wrapper-show{opacity:1}.dark .nova-slider-filter .vue-slider-rail{background-color:rgba(var(--colors-gray-700))}.dark .nova-slider-filter .vue-slider-process{background-color:rgba(var(--colors-primary-500))}.dark .nova-slider-filter .vue-slider-dot-tooltip-inner{background:rgba(var(--colors-gray-800));color:rgba(var(--colors-gray-400))}.nova-slider-filter .vue-slider-dot-tooltip-inner{background-color:rgba(var(--colors-gray-200));border-radius:.25rem;color:rgba(var(--colors-gray-500));font-size:12px;font-weight:700;margin-bottom:-5px;padding:2px 5px}',""]);const o=i},645:t=>{"use strict";t.exports=function(t){var e=[];return e.toString=function(){return this.map((function(e){var r=t(e);return e[2]?"@media ".concat(e[2]," {").concat(r,"}"):r})).join("")},e.i=function(t,r,n){"string"==typeof t&&(t=[[null,t,""]]);var i={};if(n)for(var o=0;o{},379:(t,e,r)=>{"use strict";var n,i=function(){return void 0===n&&(n=Boolean(window&&document&&document.all&&!window.atob)),n},o=function(){var t={};return function(e){if(void 0===t[e]){var r=document.querySelector(e);if(window.HTMLIFrameElement&&r instanceof window.HTMLIFrameElement)try{r=r.contentDocument.head}catch(t){r=null}t[e]=r}return t[e]}}(),a=[];function s(t){for(var e=-1,r=0;r{"use strict";e.Z=(t,e)=>{const r=t.__vccOpts||t;for(const[t,n]of e)r[t]=n;return r}},454:function(t,e,r){var n;"undefined"!=typeof self&&self,t.exports=(n=r(311),function(){var t={388:function(t,e){var r,n,i,o;"undefined"!=typeof self&&self,o=function(){function t(){var e=Object.getOwnPropertyDescriptor(document,"currentScript");if(!e&&"currentScript"in document&&document.currentScript)return document.currentScript;if(e&&e.get!==t&&document.currentScript)return document.currentScript;try{throw new Error}catch(t){var r,n,i,o=/@([^@]*):(\d+):(\d+)\s*$/gi,a=/.*at [^(]*\((.*):(.+):(.+)\)$/gi.exec(t.stack)||o.exec(t.stack),s=a&&a[1]||!1,l=a&&a[2]||!1,u=document.location.href.replace(document.location.hash,""),c=document.getElementsByTagName("script");s===u&&(r=document.documentElement.outerHTML,n=new RegExp("(?:[^\\n]+?\\n){0,"+(l-2)+"}[^<]* 136 | 137 | 175 | -------------------------------------------------------------------------------- /resources/js/filter.js: -------------------------------------------------------------------------------- 1 | import SliderFilter from './components/Filter.vue' 2 | 3 | Nova.booting((app, store) => { 4 | app.component('nova-slider-filter', SliderFilter) 5 | }) 6 | -------------------------------------------------------------------------------- /screenshots/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcasia/nova-slider-filter/81004eb53126ff51ee6ad6388939f9a3405cbf49/screenshots/dark.png -------------------------------------------------------------------------------- /screenshots/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcasia/nova-slider-filter/81004eb53126ff51ee6ad6388939f9a3405cbf49/screenshots/light.png -------------------------------------------------------------------------------- /src/FilterServiceProvider.php: -------------------------------------------------------------------------------- 1 | withMeta([ 'mode' => 'range', 'min' => min($values), 'max' => max($values), 'values' => $values ]); 18 | } 19 | 20 | public function single(int $min, int $max): self 21 | { 22 | return $this->withMeta([ 'mode' => 'single', 'min' => $min, 'max' => $max ]); 23 | } 24 | 25 | public function interval(int $value): self 26 | { 27 | return $this->withMeta([ 'interval' => $value ]); 28 | } 29 | 30 | public function minRange(int $min): self 31 | { 32 | return $this->withMeta([ 'minRange' => $min ]); 33 | } 34 | 35 | public function maxRange(int $max): self 36 | { 37 | return $this->withMeta([ 'maxRange' => $max ]); 38 | } 39 | 40 | /** 41 | * Possible values: none, hover, always, active 42 | */ 43 | public function tooltip(string $type = 'always'): self 44 | { 45 | return $this->withMeta([ 'tooltip' => $type ]); 46 | } 47 | 48 | /** 49 | * @see https://nightcatsama.github.io/vue-slider-component/#/basics/marks 50 | */ 51 | public function marks(array|bool $marks): self 52 | { 53 | return $this->withMeta([ 'marks' => $marks ]); 54 | } 55 | 56 | public function enableCross(): self 57 | { 58 | return $this->withMeta([ 'enableCross' => true ]); 59 | } 60 | 61 | public function disableMinMaxLabels(): self 62 | { 63 | return $this->withMeta([ 'disableMinMaxLabels' => true ]); 64 | } 65 | 66 | public function label(string $label): self 67 | { 68 | return $this->withMeta([ 'label' => $label ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | content: [ './resources/**/*.vue' ], 4 | important: '.nova-slider-filter' 5 | } 6 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix') 2 | 3 | require('./nova.mix') 4 | require('mix-tailwindcss') 5 | 6 | mix 7 | .setPublicPath('dist') 8 | .js('resources/js/filter.js', 'js') 9 | .vue({ version: 3 }) 10 | .postCss('resources/css/filter.css', 'css') 11 | .tailwind() 12 | .nova('digital-creative/nova-slider-filter') 13 | --------------------------------------------------------------------------------