__package__ = 'archivebox.core' import json from django import forms from django.utils.html import escape class TagEditorWidget(forms.Widget): """ A widget that renders tags as clickable pills with inline editing. - Displays existing tags alphabetically as styled pills with X remove button - Text input with HTML5 datalist for autocomplete suggestions - Press Enter or Space to create new tags (auto-creates if doesn't exist) - Uses AJAX for autocomplete and tag creation """ template_name = None # We render manually class Media: css = {'all': []} js = [] def __init__(self, attrs=None, snapshot_id=None): self.snapshot_id = snapshot_id super().__init__(attrs) def _escape(self, value): """Escape HTML entities in value.""" return escape(str(value)) if value else '' def render(self, name, value, attrs=None, renderer=None): """ Render the tag editor widget. Args: name: Field name value: Can be: - QuerySet of Tag objects (from M2M field) - List of tag names - Comma-separated string of tag names - None attrs: HTML attributes renderer: Not used """ # Parse value to get list of tag names tags = [] if value: if hasattr(value, 'all'): # QuerySet tags = sorted([tag.name for tag in value.all()]) elif isinstance(value, (list, tuple)): if value and hasattr(value[0], 'name'): # List of Tag objects tags = sorted([tag.name for tag in value]) else: # List of strings or IDs # Could be tag IDs from form submission from archivebox.core.models import Tag tag_names = [] for v in value: if isinstance(v, str) and not v.isdigit(): tag_names.append(v) else: try: tag = Tag.objects.get(pk=v) tag_names.append(tag.name) except (Tag.DoesNotExist, ValueError): if isinstance(v, str): tag_names.append(v) tags = sorted(tag_names) elif isinstance(value, str): tags = sorted([t.strip() for t in value.split(',') if t.strip()]) widget_id = attrs.get('id', name) if attrs else name # Build pills HTML pills_html = '' for tag in tags: pills_html += f''' {self._escape(tag)} ''' # Build the widget HTML html = f'''