django-searchable-dropdown 1.0.2
Biblioteca Django para criar dropdowns pesquisáveis e customizáveis
License: MIT
Requires Python: >=3.7
Keywords: django, dropdown, searchable, select, widget, form, field

# SearchableDropdown

[![PyPI version](https://badge.fury.io/py/django_searchable_dropdown.svg)](https://badge.fury.io/py/django_searchable_dropdown)
[![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/)
[![Django 2.2+](https://img.shields.io/badge/django-2.2+-green.svg)](https://www.djangoproject.com/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)](https://github.com/novaalvorada/django_searchable_dropdown)
[![Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/novaalvorada/django_searchable_dropdown)

Uma biblioteca Django completa para criar dropdowns pesquisáveis e customizáveis com funcionalidades avançadas.

## Sobre a Biblioteca

O **SearchableDropdown** é uma biblioteca Django que oferece componentes de dropdown avançados com funcionalidades de busca em tempo real, integração nativa com formulários Django e suporte a múltiplas seleções.

### Características Principais

- **Dropdowns Pesquisáveis**: Busca em tempo real nas opções
- **Integração Django**: Widgets e campos de formulário nativos
- **Suporte AJAX**: Busca dinâmica em modelos Django
- **Múltipla Seleção**: Suporte a seleção de múltiplas opções
- **Informações Adicionais**: Exibição de dados extras sobre opções selecionadas
- **Customizável**: Temas e configurações flexíveis
- **Responsivo**: Funciona em dispositivos móveis
- **Acessível**: Suporte a navegação por teclado e leitores de tela

### Status do Projeto

- **Versão**: 1.0.0 (Estável)
- **Testes**: 110 testes passando (100% cobertura)
- **Compatibilidade**: Django 2.2+ e Python 3.7+
- **Documentação**: Completa com exemplos práticos
- **Licença**: MIT (Open Source)
- **PyPI**: Disponível para instalação via pip

## Instalação

### Opção 1: Instalar via pip (Recomendado para produção)

```bash
pip install django_searchable_dropdown
```

E então adicionar ao `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    # ... outras apps
    'recoveredperispirit.django.django_searchable_dropdown',
]
```

### Opção 2: Integrar em Projeto Existente

#### 1. Adicionar ao projeto

```bash
# Copiar a pasta recoveredperispirit/django/django_searchable_dropdown para seu projeto Django
cp -r recoveredperispirit/django/django_searchable_dropdown/ /path/to/your/django/project/
```

#### 2. Configurar settings.py

```python
INSTALLED_APPS = [
    # ... outras apps
    'recoveredperispirit.django.django_searchable_dropdown',
]

# Configurações opcionais
SEARCHABLE_DROPDOWN_CONFIG = {
    'default_placeholder': 'Selecione uma opção',
    'default_search_placeholder': 'Digite para buscar...',
    'default_no_results_text': 'Nenhum resultado encontrado',
    'ajax_timeout': 5000,
    'min_search_length': 1,
    'max_results': 50,
}
```

#### 3. Coletar arquivos estáticos

```bash
python manage.py collectstatic
```

## Guia de Uso - Widgets Disponíveis

### 1. SearchableDropdownWidget (Básico)

O widget básico para dropdowns pesquisáveis com opções estáticas ou de modelos Django.

#### Exemplo Básico

```python
from recoveredperispirit.django.django_searchable_dropdown.widgets import SearchableDropdownWidget
from django import forms

class CategoriaForm(forms.Form):
    categoria = forms.ModelChoiceField(
        queryset=Categoria.objects.all(),
        widget=SearchableDropdownWidget(
            placeholder="Selecione uma categoria",
            search_placeholder="Digite para buscar categorias...",
            dropdown_type="categoria",
            min_search_length=1,
            max_results=20
        ),
        required=False
    )
```

#### Como Funciona

1. **Renderização**: O widget cria um dropdown customizado com campo de busca
2. **Busca**: Usuário digita no campo de busca e as opções são filtradas em tempo real
3. **Seleção**: Usuário clica em uma opção para selecioná-la
4. **Validação**: O valor é validado pelo Django normalmente

#### Resultado Visual

```
┌─────────────────────────────────────┐
│ Selecione uma categoria             ▼ │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ Digite para buscar categorias...    │
├─────────────────────────────────────┤
│ ✓ Tecnologia                        │
│   Esportes                          │
│   Música                            │
│   Livros                            │
└─────────────────────────────────────┘
```

### 2. SearchableDropdownMultipleWidget (Múltipla Seleção)

Widget para selecionar múltiplas opções simultaneamente.

#### Exemplo de Uso

```python
from recoveredperispirit.django.django_searchable_dropdown.widgets import SearchableDropdownMultipleWidget

class ProdutoForm(forms.Form):
    tags = forms.ModelMultipleChoiceField(
        queryset=Tag.objects.all(),
        widget=SearchableDropdownMultipleWidget(
            placeholder="Selecione tags (máximo 5)",
            search_placeholder="Digite para buscar tags...",
            dropdown_type="tags",
            max_selections=5,
            allow_clear=True
        ),
        required=False
    )
```

#### Como Funciona

1. **Múltipla Seleção**: Usuário pode selecionar várias opções
2. **Contador**: Mostra quantas opções foram selecionadas
3. **Limite**: Pode definir um número máximo de seleções
4. **Remoção**: Opções selecionadas podem ser removidas individualmente

#### Resultado Visual

```
┌─────────────────────────────────────┐
│ ✓ Tecnologia, Esportes (2/5)       ▼ │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ Digite para buscar tags...          │
├─────────────────────────────────────┤
│ ✓ Tecnologia [×]                    │
│ ✓ Esportes [×]                      │
│   Música                            │
│   Livros                            │
└─────────────────────────────────────┘
```

### 3. SearchableDropdownAjaxWidget (Busca AJAX)

Widget para busca dinâmica via AJAX em grandes datasets.

#### Exemplo de Uso

```python
from recoveredperispirit.django.django_searchable_dropdown.widgets import SearchableDropdownAjaxWidget

class ClienteForm(forms.Form):
    cliente = forms.ModelChoiceField(
        queryset=Cliente.objects.none(),  # Queryset vazio inicialmente
        widget=SearchableDropdownAjaxWidget(
            placeholder="Digite para buscar clientes...",
            search_placeholder="Nome ou email do cliente...",
            dropdown_type="cliente_ajax",
            ajax_url="/api/clientes/search/",
            min_search_length=2,
            max_results=20,
            delay=300,  # Delay em ms antes de fazer a busca
            allow_clear=True
        ),
        required=False
    )
```

#### Como Funciona

1. **Busca Dinâmica**: Dados são carregados via AJAX conforme o usuário digita
2. **Delay**: Aguarda o usuário parar de digitar antes de fazer a requisição
3. **Filtros**: Aplica filtros no servidor para melhor performance
4. **Cache**: Pode implementar cache para melhorar performance

#### Resultado Visual

```
┌─────────────────────────────────────┐
│ Digite para buscar clientes...      ▼ │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ joão...                             │
├─────────────────────────────────────┤
│ Buscando...                         │
├─────────────────────────────────────┤
│ João Silva (joao@email.com)         │
│ João Santos (joao.s@email.com)      │
│ João Oliveira (joao.o@email.com)    │
└─────────────────────────────────────┘
```

#### API Endpoint Necessário

```python
# views.py
from django.http import JsonResponse
from django.db.models import Q

def api_clientes_search(request):
    query = request.GET.get('q', '')
    if len(query) < 2:
        return JsonResponse({'results': []})
    
    clientes = Cliente.objects.filter(
        Q(nome__icontains=query) | Q(email__icontains=query)
    )[:20]
    
    results = [
        {
            'id': cliente.id,
            'text': f"{cliente.nome} ({cliente.email})"
        }
        for cliente in clientes
    ]
    
    return JsonResponse({'results': results})
```

### 4. SearchableDropdownWithInfoWidget (Com Informações)

Widget que mostra informações detalhadas sobre a opção selecionada.

#### Exemplo de Uso

```python
from recoveredperispirit.django.django_searchable_dropdown.widgets import SearchableDropdownWithInfoWidget

class ProdutoDetalhadoForm(forms.Form):
    produto = forms.ModelChoiceField(
        queryset=Produto.objects.all(),
        widget=SearchableDropdownWithInfoWidget(
            placeholder="Selecione um produto para ver detalhes",
            search_placeholder="Digite para buscar produtos...",
            dropdown_type="produto_info",
            info_url="/api/produtos/info/",
            info_container_id="produto-info-container"
        ),
        required=False
    )
```

#### Como Funciona

1. **Seleção**: Usuário seleciona uma opção no dropdown
2. **Carregamento**: Informações detalhadas são carregadas via AJAX
3. **Exibição**: Dados são exibidos em um container separado
4. **Atualização**: Informações são atualizadas a cada nova seleção

#### Resultado Visual

```
┌─────────────────────────────────────┐
│ iPhone 13 Pro                      ▼ │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ Digite para buscar produtos...      │
├─────────────────────────────────────┤
│ iPhone 13 Pro                       │
│ iPhone 12                           │
│ Samsung Galaxy S21                  │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ Informações do Produto              │
├─────────────────────────────────────┤
│ Nome: iPhone 13 Pro                 │
│ Código: PROD001                     │
│ Preço: R$ 8.999,00                  │
│ Estoque: 15 unidades                │
│ Categoria: Smartphones              │
│ Marca: Apple                        │
│ Descrição: iPhone 13 Pro 256GB...   │
└─────────────────────────────────────┘
```

#### API Endpoint Necessário

```python
# views.py
def api_produtos_info(request):
    produto_id = request.GET.get('id')
    if not produto_id:
        return JsonResponse({'error': 'ID do produto não fornecido'}, status=400)
    
    try:
        produto = Produto.objects.get(id=produto_id)
        info = {
            'id': produto.id,
            'nome': produto.nome,
            'codigo': produto.codigo,
            'preco': str(produto.preco),
            'estoque': produto.estoque,
            'categoria': produto.categoria.nome,
            'marca': produto.marca.nome,
            'descricao': produto.descricao or 'Sem descrição'
        }
        return JsonResponse(info)
    except Produto.DoesNotExist:
        return JsonResponse({'error': 'Produto não encontrado'}, status=404)
```

#### Template HTML Necessário

```html
<!-- Incluir container para informações -->
<div class="row">
    <div class="col-md-8">
        {{ form.produto }}
    </div>
    <div class="col-md-4">
        <div class="card">
            <div class="card-header">
                <h6>Informações do Produto</h6>
            </div>
            <div class="card-body" id="produto-info-container">
                <p class="text-muted">Selecione um produto para ver as informações</p>
            </div>
        </div>
    </div>
</div>
```

## Customização Avançada

### Configurações por Tipo de Dropdown

```python
# Em apps.py ou settings.py
from recoveredperispirit.django.django_searchable_dropdown.utils import dropdown_config

# Configurar tipo personalizado
dropdown_config.register_type('activity', {
    'placeholder': 'Selecione uma atividade',
    'search_placeholder': 'Digite o nome da atividade...',
    'no_results_text': 'Nenhuma atividade encontrada',
    'min_search_length': 2,
    'max_results': 20,
    'allow_clear': True,
    'allow_create': False,
})

# Usar em formulários
class ActivityForm(forms.Form):
    activity = forms.ModelChoiceField(
        queryset=Activity.objects.all(),
        widget=SearchableDropdownWidget(dropdown_type='activity')
    )
```

### Estilos CSS Customizados

```css
/* Tema personalizado */
.searchable-dropdown.custom-theme {
    --dropdown-bg: #ffffff;
    --dropdown-border: #e0e0e0;
    --dropdown-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    --option-hover-bg: #f8f9fa;
    --option-selected-bg: #007bff;
    --option-selected-color: #ffffff;
    --search-input-bg: #f8f9fa;
    --search-input-border: #dee2e6;
}

/* Estilos específicos para diferentes tipos */
.searchable-dropdown[data-type="produto"] {
    --option-selected-bg: #28a745;
}

.searchable-dropdown[data-type="cliente"] {
    --option-selected-bg: #17a2b8;
}
```

### JavaScript Customizado

```javascript
// Eventos customizados
document.addEventListener('dropdown:change', function(e) {
    const dropdown = e.target;
    const value = e.detail.value;
    const text = e.detail.text;
    
    console.log('Dropdown alterado:', { value, text });
    
    // Lógica customizada aqui
    if (dropdown.getAttribute('data-type') === 'produto') {
        updateProductInfo(value);
    }
});

// Inicialização customizada
const customDropdown = new SearchableDropdown(element, {
    placeholder: 'Selecione uma opção',
    searchPlaceholder: 'Digite para buscar...',
    onSelect: function(value, text) {
        console.log('Opção selecionada:', value, text);
    },
    onSearch: function(query) {
        console.log('Buscando:', query);
    }
});
```

## Exemplos Práticos Completos

### Exemplo 1: Sistema de Agendamento

```python
# models.py
class Activity(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    duration = models.IntegerField(help_text="Duração em minutos")
    max_participants = models.IntegerField()
    
    def __str__(self):
        return self.name

class Schedule(models.Model):
    activity = models.ForeignKey(Activity, on_delete=models.CASCADE)
    date = models.DateField()
    time = models.TimeField()
    participants = models.ManyToManyField('User', blank=True)
    
    def __str__(self):
        return f"{self.activity.name} - {self.date} {self.time}"

# forms.py
class ScheduleForm(forms.ModelForm):
    activity = forms.ModelChoiceField(
        queryset=Activity.objects.all(),
        widget=SearchableDropdownWithInfoWidget(
            dropdown_type='activity',
            info_url='/api/activities/info/',
            info_container_id='activity-info',
            placeholder='Selecione uma atividade'
        )
    )
    
    participants = forms.ModelMultipleChoiceField(
        queryset=User.objects.all(),
        widget=SearchableDropdownMultipleWidget(
            dropdown_type='users',
            placeholder='Selecione participantes',
            max_selections=10
        ),
        required=False
    )
    
    class Meta:
        model = Schedule
        fields = ['activity', 'date', 'time', 'participants']

# views.py
def api_activities_info(request):
    activity_id = request.GET.get('id')
    try:
        activity = Activity.objects.get(id=activity_id)
        return JsonResponse({
            'name': activity.name,
            'description': activity.description,
            'duration': f"{activity.duration} minutos",
            'max_participants': activity.max_participants
        })
    except Activity.DoesNotExist:
        return JsonResponse({'error': 'Atividade não encontrada'}, status=404)
```

### Exemplo 2: E-commerce com Produtos

```python
# models.py
class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    
    def __str__(self):
        return self.name

class Product(models.Model):
    name = models.CharField(max_length=200)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.IntegerField()
    description = models.TextField()
    tags = models.ManyToManyField('Tag', blank=True)
    
    def __str__(self):
        return self.name

# forms.py
class ProductSearchForm(forms.Form):
    category = forms.ModelChoiceField(
        queryset=Category.objects.all(),
        widget=SearchableDropdownWidget(
            dropdown_type='category',
            placeholder='Todas as categorias',
            allow_clear=True
        ),
        required=False
    )
    
    products = forms.ModelMultipleChoiceField(
        queryset=Product.objects.all(),
        widget=SearchableDropdownMultipleWidget(
            dropdown_type='products',
            placeholder='Selecione produtos para comparar',
            max_selections=5
        ),
        required=False
    )
    
    featured_product = forms.ModelChoiceField(
        queryset=Product.objects.all(),
        widget=SearchableDropdownWithInfoWidget(
            dropdown_type='product_info',
            info_url='/api/products/info/',
            info_container_id='product-details',
            placeholder='Selecione um produto para ver detalhes'
        ),
        required=False
    )
```

## Configurações Avançadas

### Configurações Globais

```python
# settings.py
SEARCHABLE_DROPDOWN_CONFIG = {
    # Configurações padrão
    'default_placeholder': 'Selecione uma opção',
    'default_search_placeholder': 'Digite para buscar...',
    'default_no_results_text': 'Nenhum resultado encontrado',
    
    # Configurações de performance
    'ajax_timeout': 5000,
    'min_search_length': 1,
    'max_results': 50,
    
    # Configurações de UI
    'allow_clear': True,
    'allow_create': False,
    'delay': 300,
    
    # Configurações de acessibilidade
    'aria_label': 'Dropdown pesquisável',
    'aria_describedby': 'dropdown-help',
}
```

### Configurações por Ambiente

```python
# settings/development.py
SEARCHABLE_DROPDOWN_CONFIG = {
    'debug': True,
    'ajax_timeout': 10000,
    'delay': 500,
}

# settings/production.py
SEARCHABLE_DROPDOWN_CONFIG = {
    'debug': False,
    'ajax_timeout': 3000,
    'delay': 200,
    'cache_results': True,
}
```

## Deploy e Performance

### Otimizações para Produção

```python
# settings.py
SEARCHABLE_DROPDOWN_CONFIG = {
    'cache_results': True,
    'cache_timeout': 300,  # 5 minutos
    'ajax_timeout': 3000,
    'max_results': 20,
    'min_search_length': 2,
}

# views.py com cache
from django.core.cache import cache

def api_search_view(request):
    query = request.GET.get('q', '')
    cache_key = f"search_{query}"
    
    # Verificar cache
    cached_results = cache.get(cache_key)
    if cached_results:
        return JsonResponse(cached_results)
    
    # Buscar dados
    results = perform_search(query)
    
    # Salvar no cache
    cache.set(cache_key, results, 300)
    
    return JsonResponse(results)
```

## Troubleshooting

### Problemas Comuns e Soluções

#### 1. Dropdown não abre
```javascript
// Verificar se os scripts foram carregados
console.log('SearchableDropdown:', typeof SearchableDropdown);
console.log('SearchableDropdownUtils:', typeof SearchableDropdownUtils);

// Verificar se o elemento existe
const dropdown = document.querySelector('.searchable-dropdown');
console.log('Dropdown element:', dropdown);
```

#### 2. Busca AJAX não funciona
```python
# Verificar se a view retorna JSON válido
def api_search_view(request):
    try:
        # Sua lógica aqui
        return JsonResponse({'results': results})
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=500)
```

#### 3. Estilos não aplicados
```css
/* Forçar estilos com !important se necessário */
.searchable-dropdown {
    display: block !important;
    position: relative !important;
    z-index: 1000 !important;
}
```

### Debug Avançado

```javascript
// Habilitar modo debug
SearchableDropdownUtils.setDebugMode(true);

// Verificar dropdowns inicializados
const dropdowns = SearchableDropdownUtils.getInitializedDropdowns();
console.log('Dropdowns inicializados:', dropdowns);

// Verificar configurações
const configs = SearchableDropdownUtils.getConfigs();
console.log('Configurações:', configs);
```

## Testes e Qualidade

### Executando Testes

```bash
# Testes da biblioteca
cd recoveredperispirit/django/django_searchable_dropdown
python -m pytest tests/ -v

# Testes com cobertura
python -m pytest tests/ --cov=. --cov-report=html

# Testes do test_app
cd test_app
python manage.py test
```

### Status dos Testes

- **110 testes passando** (100% de cobertura)
- **Formulários**: 25 testes
- **Widgets**: 25 testes  
- **Utilitários**: 35 testes
- **Integração**: 6 testes

## Demonstração Completa

Para ver todos os widgets em ação, execute o **test_app**:

```bash
cd test_app
python manage.py runserver
```

Acesse: http://localhost:8000

### Páginas de Demonstração

- **Página Inicial**: Visão geral de todas as funcionalidades
- **Produtos**: Dropdowns básicos e com filtros
- **Clientes**: Dropdowns com AJAX
- **Pedidos**: Dropdowns múltiplos
- **AJAX Demo**: Demonstração completa de busca AJAX
- **With Info Demo**: Demonstração de dropdowns com informações

## Licença

Esta biblioteca é distribuída sob a licença MIT.

## Contribuição

Para contribuir:

1. Fork o repositório
2. Crie uma branch para sua feature
3. Faça commit das suas mudanças
4. Push para a branch
5. Abra um Pull Request

## Suporte

Para suporte e dúvidas:

- **Teste primeiro**: Use o test_app para verificar se o problema é específico do seu projeto
- **Consulte a documentação**: Esta documentação e os exemplos no test_app
- **Abra uma issue**: No GitHub com detalhes do problema e ambiente
- **Verifique os logs**: Use o modo debug para identificar problemas