Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ composer require codewithdennis/filament-select-tree
## Features
- ✅ Compatible with dark mode
- ✅ Featuring search functionality
- ❌ Multi-select (Coming soon)
- ❌ Relationships
- ✅ Comma seperated multi-select
- ❌ Relationships (Planned)

## Usage

Expand All @@ -32,15 +32,34 @@ SelectTree::make('category_id')
->tree(Category::class, 'category_id', 'name', function ($query) {
return $query;
})

// The label 'Category' is assigned to the field.
->label(__('Category'))

// Set a custom placeholder for when no items are selected
->placeholder(__('Your custom placeholder here'))

// Ensures that only leaf nodes can be selected while preventing the selection of groups.
->disabledBranchNode()

// Show the count of children alongside the group's name.
->withCount()

// To disable tags and display a text message instead (e.g., "X items have been selected")
// Tags is always disabled on single select
->disableTags()

// To keep the dropdown open at all times
->alwaysOpen()

// By default, all nodes are independent. Set this to false if you want to display groups when all subnodes are selected.
->showGroupsWhenAllSelected(false)

// By default, the clearable icon is enabled, but you can hide it with:
->clearable(false)

// Enable the option to save multiple values as a string (comma-separated)
->multiple()

// Activates the search functionality for the SelectTree.
->searchable()
Expand Down
87 changes: 87 additions & 0 deletions resources/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
outline: 2px solid transparent;
outline-offset: 2px;
background: transparent;
padding: 8px;
}

.treeselect {
Expand Down Expand Up @@ -107,4 +108,90 @@ html.dark .treeselect-list {

html.dark .treeselect-list.treeselect-list--single-select html.dark.treeselect-list__item--single-selected, html.dark .treeselect-list__item--focused, html.dark .treeselect-list__item:hover, html.dark .treeselect-list.treeselect-list--single-select .treeselect-list__item--single-selected, html.dark .treeselect-list__item--focused, html.dark .treeselect-list__item:hover {
background-color: hsla(0, 0%, 100%, .05) !important;
}

html.dark .treeselect-list__item--checked, .treeselect-list__item--checked {
background: transparent;
}

.treeselect-input__tags-element {
--tw-bg-opacity: 1;
--tw-text-opacity: 1;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
--tw-ring-inset: inset;
--tw-ring-color: rgba(var(--primary-600), 0.1);
align-items: center;
background-color: rgba(var(--primary-50), var(--tw-bg-opacity));
border-radius: 0.375rem;
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
color: rgba(var(--primary-600), var(--tw-text-opacity));
display: inline-flex;
font-size: .75rem;
font-weight: 500;
gap: 0.25rem;
line-height: 1rem;
padding: 0.25rem 0.5rem;
word-break: break-all;
}

html.dark .treeselect-input__tags-element {
--tw-text-opacity: 1;
--tw-ring-color: rgba(var(--primary-400), 0.3);
background-color: rgba(var(--primary-400), .1);
color: rgba(var(--primary-400), var(--tw-text-opacity));
}

.treeselect-list__item-checkbox-container {
border-radius: 0.25rem;
height: 16px;
min-width: 16px;
width: 16px;
}

.treeselect-list__item--checked .treeselect-list__item-checkbox-container, .treeselect-list__item--partial-checked .treeselect-list__item-checkbox-container {
background-color: rgba(var(--primary-600), var(--tw-text-opacity));
}

.treeselect-list__item-checkbox {
transition-duration: 75ms;
background-color: transparent !important;
border: none;
}

.treeselect-list__item-checkbox-container {
background-color: #f8f5f5;
border: none;
}

html.dark .treeselect-list__item-checkbox-container {
border: rgb(255 255 255/var(--tw-text-opacity));
}

html.dark .treeselect-list__item-checkbox-container {
background-color: hsla(0, 0%, 100%, .05);
}

.treeselect-list__item-checkbox-icon {
height: 80%;
left: 0.1rem;
top: 0.1rem;
width: 80%;
}

.treeselect-input__tags-element:hover {
background-color: rgba(0, 0, 0, 0.1);
}

.treeselect-input__tags-element:hover {
background-color: rgba(var(--primary-600), var(--tw-text-opacity));
color: white;
}

.treeselect-input__tags-element:hover .treeselect-input__tags-cross svg {
stroke: rgb(255 255 255/var(--tw-text-opacity));
}

.treeselect-input__tags {
margin-left: 3px;
}
2 changes: 1 addition & 1 deletion resources/dist/custom.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion resources/dist/tree.js

Large diffs are not rendered by default.

42 changes: 27 additions & 15 deletions resources/js/index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import Treeselect from 'treeselectjs'

export default function tree({
state,
name,
options,
searchable,
showCount,
placeholder,
disabledBranchNode,
disabled = false,
isSingleSelect = true,
showTags = false,
clearable = false,
}) {
state,
name,
options,
searchable,
showCount,
placeholder,
disabledBranchNode,
disabled = false,
isSingleSelect = true,
showTags = true,
clearable = true,
isIndependentNodes = true,
alwaysOpen = false
}) {
return {
state,
tree: null,
init() {
const values = this.isSingleSelect
? (this.state !== null ? this.state : '')
: (this.state !== null ? this.state.split(',').map(Number) : '');

this.tree = new Treeselect({
id: `tree-${name}-id`,
ariaLabel: `tree-${name}-label`,
parentHtmlContainer: this.$refs.tree,
value: this.state,
value: values,
options,
searchable,
showCount,
Expand All @@ -30,11 +36,17 @@ export default function tree({
disabled,
isSingleSelect,
showTags,
clearable
clearable,
isIndependentNodes,
alwaysOpen,
});

this.tree.srcElement.addEventListener('input', (e) => {
this.state = e.detail;
if (Array.isArray(e.detail)) {
this.state = e.detail.join(",");
} else {
this.state = e.detail;
}
});
}
}
Expand Down
19 changes: 12 additions & 7 deletions resources/views/select-tree.blade.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
<div
wire:ignore
x-data
x-load-css="[
wire:ignore
x-data
x-load-css="[
@js(\Filament\Support\Facades\FilamentAsset::getStyleHref('tree', package: 'codewithdennis/filament-select-tree')),
@js(\Filament\Support\Facades\FilamentAsset::getStyleHref('custom', package: 'codewithdennis/filament-select-tree'))
]"
>
<div
x-ignore
ax-load="visible"
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('tree', package: 'codewithdennis/filament-select-tree') }}"
x-data="tree({
x-ignore
ax-load="visible"
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('tree', package: 'codewithdennis/filament-select-tree') }}"
x-data="tree({
name: '{{ $getName() }}',
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$getStatePath()}')") }},
options: {{ json_encode($getTree()) }},
Expand All @@ -20,6 +20,11 @@
placeholder: '{{ $getPlaceholder() }}',
disabledBranchNode: '{{ $getDisabledBranchNode() }}',
disabled: '{{ $isDisabled() }}',
isSingleSelect: '{{ !$getMultiple() }}',
isIndependentNodes: '{{ $getIndependent() }}',
showTags: '{{ $getMultiple() && $getShowTags() }}',
alwaysOpen: '{{ $getAlwaysOpen() }}',
clearable: '{{ $getClearable() }}',
})"
>
<div x-ref="tree"></div>
Expand Down
70 changes: 70 additions & 0 deletions src/SelectTree.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ class SelectTree extends Field

protected bool $withCount = false;

protected bool $multiple = false;

protected bool $alwaysOpen = false;

protected bool $showTags = true;

protected bool $independent = true;

protected bool $clearable = true;

protected bool $disabledBranchNode = false;

protected string $treeModel;
Expand All @@ -36,6 +46,41 @@ public function withCount(bool $withCount = true): static
return $this;
}

public function clearable(bool $clearable = true): static
{
$this->clearable = $clearable;

return $this;
}

public function independent(bool $independent = true): static
{
$this->independent = $independent;

return $this;
}

public function showTags(bool $showTags = true): static
{
$this->showTags = $showTags;

return $this;
}

public function alwaysOpen(bool $alwaysOpen = true): static
{
$this->alwaysOpen = $alwaysOpen;

return $this;
}

public function multiple(bool $multiple = true): static
{
$this->multiple = $multiple;

return $this;
}

public function disabledBranchNode(bool $disabledBranchNode = true): static
{
$this->disabledBranchNode = $disabledBranchNode;
Expand All @@ -48,11 +93,36 @@ public function getTree(): Collection
return $this->evaluate($this->buildTree());
}

public function getIndependent(): bool
{
return $this->evaluate($this->independent);
}

public function getWithCount(): bool
{
return $this->evaluate($this->withCount);
}

public function getMultiple(): bool
{
return $this->evaluate($this->multiple);
}

public function getClearable(): bool
{
return $this->evaluate($this->clearable);
}

public function getAlwaysOpen(): bool
{
return $this->evaluate($this->alwaysOpen);
}

public function getShowTags(): bool
{
return $this->evaluate($this->showTags);
}

public function getDisabledBranchNode(): bool
{
return $this->evaluate($this->disabledBranchNode);
Expand Down