Tucano v0.37.2

Date picker

Convierte un <input type="text"> en un campo de fecha con calendario, rango y hora. En pantalla aparece el formato del idioma de la página; en el formulario va ISO, que es lo que Django espera. La escritura con máscara sigue valiendo, para quien escribe más rápido de lo que hace clic.

Ejemplos

El mismo <input> con atributos distintos. Debajo de cada campo aparece el valor ISO, que es lo que llega al servidor.

Fecha y hora

data-time="true" añade las columnas de hora y minuto, de 5 en 5.

Rango con hora

Con hora el panel no se cierra solo: el botón Aplicar confirma.

Minuto a minuto, con segundos

data-minute-step="1" lista de 00 a 59; data-seconds="true" añade la tercera columna.

Límites

data-min y data-max en aaaa-mm-dd. Fuera de ellos el día queda desactivado.

Solo días hábiles, a partir de hoy

disabledDates recibe una función y no existe como atributo — va en JavaScript.

Atajos de rango

data-presets="true" activa Hoy, Últimos 7 días, Este mes y los demás.

Atajos propios

Una lista de { label, value() }, en JavaScript.

Número de semana

data-week-numbers="true" muestra la semana ISO-8601 a la izquierda.

Otro idioma

data-locale="en-US": mes antes del día, semana que empieza el domingo, nombres en inglés.

Formato propio

data-format="dd.MM.yyyy". La máscara sigue el formato sola.

Con icono

El .tuc-input-group del formulario, con un SVG antes del campo.

Con error

aria-invalid="true" en el campo — Django 5 ya lo escribe.

La fecha tiene que ser posterior a hoy.

Cómo usar

Marca el <input> y se inicializa solo al cargar y en cada htmx:afterSwap. El componente pone la clase .tuc-input en el campo, así que no hace falta escribirla.

<input type="text" name="date" data-tuc-datepicker>
<input type="text" name="when" data-tuc-datepicker data-time="true">
<input type="text" name="period" data-tuc-datepicker data-mode="range">
<input type="text" name="window" data-tuc-datepicker data-mode="range" data-time="true">
AtributoPor defectoPara qué
data-modesinglerange para rango
data-timefalseAñade el selector de hora
data-minute-step5Paso de la columna de minutos; 1 los lista todos
data-secondsfalseColumna de segundos
data-min data-max—Límites, en aaaa-mm-dd
data-months1, o 2 en rangoCuántos meses lado a lado
data-presetsfalsetrue activa los atajos — solo en modo rango
data-week-numbersfalseColumna con la semana ISO
data-locale<html lang>Nombres, orden de día y mes, inicio de la semana y reloj de 12 o 24 horas
data-formatdel idiomaFormato de visualización, como dd/MM/yyyy
data-iso-nameel namename del campo oculto con el ISO
data-placementbottom-centerLado y alineación del panel
data-nativefalsetrue usa el selector del sistema

En el formulario de Django

class EventForm(forms.ModelForm):
    class Meta:
        model = Event
        fields = ["date", "start", "period"]
        widgets = {
            "date": forms.TextInput(attrs={"data-tuc-datepicker": ""}),
            "start": forms.TextInput(attrs={"data-tuc-datepicker": "", "data-time": "true"}),
            "period": forms.TextInput(attrs={"data-tuc-datepicker": "", "data-mode": "range"}),
        }

No uses type="date" en el widget: activaría el calendario del navegador encima del nuestro. El valor inicial puede venir en ISO o en el formato del idioma — el componente lee los dos.

En JavaScript

Para lo que no cabe en un atributo — disabledDates, atajos propios, firstDayOfWeek, autoApply, clearable, onChange — y para leer y escribir el valor.

const dp = new Tucano.DatePicker('#delivery', {
  mode: 'range',
  time: true,
  minuteStep: 15,
  min: '2026-01-01',
  disabledDates: (d) => d.getDay() === 0,   // cerrado los domingos
  onChange: (value, { iso }) => console.log(value, iso),
});

dp.setValue({ start: '2026-03-01', end: '2026-03-15' });
dp.getValue();   // { start: Date, end: Date } — en el modo simple, un Date
dp.clear();
dp.open();
dp.destroy();
// Atajos propios: value() devuelve { start, end } y se llama cada vez que se abre.
new Tucano.DatePicker('#period', {
  mode: 'range',
  presets: [
    { label: 'Próximos 7 días', value: () => {
      const today = Tucano.dates.startOfDay(new Date());
      return { start: today, end: Tucano.dates.addDays(today, 6) };
    } },
  ],
});

Valor y formulario

El campo visible muestra la fecha formateada, como 25/12/2026; un <input type="hidden"> creado al lado lleva el name y el valor en ISO. El servidor recibe ISO y nunca el texto formateado, que cambiaría con el idioma de quien lo rellenó.

POST  date    = 2026-12-25
POST  start   = 2026-12-25T09:30          # DateTimeField lo lee directamente
POST  exact   = 2026-12-25T09:30:15       # con data-seconds
POST  period  = 2026-03-01,2026-03-15     # inicio y fin, separados por coma

DateField y DateTimeField hacen el parse de esto sin configuración. El rango llega en un solo campo; sepáralo en el clean_:

class BookingForm(forms.Form):
    period = forms.CharField()

    def clean_period(self):
        start, _, end = self.cleaned_data["period"].partition(",")
        return date.fromisoformat(start), date.fromisoformat(end)

Para editar, devuelve el valor en ISO en el value — el componente lee ISO y también el formato del idioma. Con data-iso-name el campo visible se queda con su propio name y el oculto recibe otro, y los dos se envían. Un campo sin name y sin data-iso-name no recibe ningún campo oculto.

Cada elección dispara tucano:change en el campo visible, y también el change nativo, así la validación y hx-trigger="change" ven el valor.

document.querySelector('#due_date').addEventListener('tucano:change', (e) => {
  e.detail.value;      // Date, o { start, end } en el rango
  e.detail.iso;        // '2026-12-25' — el mismo texto del campo oculto
  e.detail.instance;   // el DatePicker
});

Aquí e.target.name viene vacío

El evento sale del campo visible, y el name pasó al oculto. Usa e.detail.iso, o lee el oculto, que está justo después del campo.

Escritura

El campo tiene una máscara sacada del propio formato de visualización, así que sigue al idioma sin configuración. Se escriben solo los números; la barra, los dos puntos y la raya del rango se ponen solos.

ModoMáscara en pt-BR
Fechadd/mm/aaaa
Fecha y horadd/mm/aaaa hh:mm
Rangodd/mm/aaaa — dd/mm/aaaa
Rango con horadd/mm/aaaa hh:mm — dd/mm/aaaa hh:mm

Con la máscara completa el calendario ya salta a la fecha escrita, sin cerrarse y sin cambiar el valor. El valor se confirma al salir del campo o con Enter mientras el panel está abierto, que también lo cierra; Esc descarta lo escrito. Un texto que no se convierte en fecha vuelve al valor anterior, y una fecha fuera de min, max o disabledDates no se acepta — ni se lleva al límite. Un rango sin un fin válido se rechaza entero. Borrar sobre un separador elimina el dígito vecino, en lugar de trabarse.

Un formato con nombre de mes (MMMM) o AM/PM no se puede enmascarar; ahí el campo queda libre y vale la lectura tolerante, que acepta 25/12/26, 25-12-2026, 25122026, 2512 (año en curso) y 12/25/2026 2:05 pm. Escribir solo la fecha en un campo con hora mantiene la hora que ya estaba elegida.

Hacer clic en el mes de la cabecera cambia a la cuadrícula de meses, y en el año, a la de años — así una fecha de nacimiento no cuesta treinta clics en la flecha.

En el móvil

El panel es el mismo del escritorio, adaptado al tacto: texto de 16px, celdas más grandes, un mes debajo del otro en el rango y los atajos en una franja que se desplaza en horizontal.

Los 16px no son estética: Safari de iOS hace zoom en toda la página al enfocar un campo con fuente más pequeña. Por el mismo motivo, en pantalla estrecha táctil el campo no recibe foco ni máscara — tocar abre el panel, y el teclado del sistema no sube a tapar el calendario.

El selector del propio sistema es opcional:

native: false     // por defecto: el panel en todas partes
native: 'auto'    // el selector del sistema donde el puntero es táctil
native: true      // siempre el selector del sistema — como atributo, data-native="true"

El modo nativo no cambia el type de tu campo

Cambiar a type="date" hacía que todo CSS escrito como input[type=text] dejara de coincidir, y el campo se volvía un input en bruto del navegador. Ahora un input nativo transparente queda encima, del tamaño del campo, y el POST sigue siendo el del oculto. El rango nunca usa el nativo: no existe selector de intervalo en HTML.

Teclado y accesibilidad

El campo anuncia que abre un dialog, la cuadrícula es role="grid" y cada día tiene aria-selected y el nombre completo — "viernes, 25 de diciembre de 2026". Llegar con Tab no abre el panel.

TeclaDóndeAcción
↓CampoAbre el calendario
EspacioCampo vacíoAbre el calendario
EnterCampo, panel abiertoConfirma lo que se escribió y cierra
← →CuadrículaDía anterior y siguiente
↑ ↓CuadrículaMismo día en la semana anterior y en la siguiente
PageUp PageDownCuadrículaMes anterior y siguiente; con Shift, año
Home EndCuadrículaInicio y fin de la semana
Enter EspacioCuadrículaElige el día
↑ ↓Columna de horaValor anterior y siguiente de la columna
Home EndColumna de horaPrimer y último valor de la columna
Enter EspacioColumna de horaElige el valor
TabCampo, panel abiertoPasa al siguiente campo y cierra el panel
TabDentro del panelRecorre los controles sin salir del panel; cada columna de hora es una sola parada
EscCampo o panelCierra, descarta lo que no se confirmó y devuelve el foco al campo

Por qué Tab no abre, y Enter tampoco

Quien tabula por un formulario hasta el botón de guardar no debería llevarse un calendario en la cara en cada campo, tapando el siguiente — era lo que apilaba paneles. Y este es un campo de texto dentro de un <form>: Enter ahí envía el formulario, que es lo que se espera después de escribir la fecha. Por eso quien abre es Espacio, y solo con el campo vacío, porque con hora se escribe 07/09/2026 14:30.

Un clic fuera cierra sin robar el foco de donde se hizo clic; un rango elegido a medias se descarta al cerrar, porque no existe medio intervalo, y vuelve el rango que ya estaba elegido. Con el botón Aplicar — el predeterminado con hora, o autoApply: false —, día, hora y atajo quedan pendientes: solo Aplicar confirma y dispara tucano:change, una vez, y cerrar con Esc o un clic fuera descarta la elección. Sin él, cada elección vale en el acto. Con prefers-reduced-motion el panel solo se desvanece, sin deslizarse.

API

Generada a partir del código en cada build — si algo no está aquí, no existe.

Marcado
[data-tuc-datepicker]
En JS
new Tucano.DatePicker(alvo, opcoes)
Atributos
data-format data-iso-name data-locale data-max data-min data-minute-step data-mode data-months data-native data-placement data-presets data-seconds data-time data-week-numbers
Métodos
getValue setValue clear open close toggle destroy
Eventos
tucano:change

Opciones

Las notas de esta tabla salen de los comentarios del código fuente, que están escritos en portugués.

OpciónPor defectoPara qué
mode'single''single' | 'range'
timefalsetrue habilita seletor de hora
secondsfalse
minuteStep5
localeundefineddefault: locale do documento/navegador
formatundefineddefault: padrao numerico do locale
firstDayOfWeekundefined
monthsundefineddefault: 2 em range, 1 em single
minnull
maxnull
disabledDatesnull(date) => boolean
presetsfalseatalhos de periodo (Hoje, Ultimos 7 dias...): opt-in
autoApplyundefineddefault: true sem hora, false com hora. Sem autoApply, a escolha so vale no Aplicar
clearabletrue
weekNumbersfalse
placement'bottom-center'centralizado no campo; as bordas da tela ainda mandam
appendToundefined
isoNameundefinedname do input hidden com o valor ISO
nativefalse
onChangenull
onOpennull
onClosenull