""" Card Sorting / Grouping Layout Drag items (text snippets, labels, concepts) into predefined or user-created groups. Open card sorting lets annotators create their own categories; closed card sorting provides predefined ones. Research: Spencer (2009) "Card Sorting: Designing Usable Categories"; Nielsen Norman Group. """ import json import logging from .identifier_utils import ( safe_generate_layout, generate_element_identifier, generate_validation_attribute, escape_html_content, generate_layout_attributes ) logger = logging.getLogger(__name__) def generate_card_sort_layout(annotation_scheme): """ Generate HTML for a Card Sorting interface. Args: annotation_scheme (dict): Configuration including: - name: Schema identifier - description: Display description - mode: "closed" (predefined groups) or "open" (user-created groups) - groups: List of group names (for closed mode) - items_field: Field in data containing items to sort - allow_empty_groups: Whether empty groups are OK - allow_multiple: Whether an item can appear in multiple groups Returns: tuple: (html_string, key_bindings) """ return safe_generate_layout(annotation_scheme, _generate_card_sort_layout_internal) def _generate_card_sort_layout_internal(annotation_scheme): schema_name = annotation_scheme['name'] description = annotation_scheme['description'] mode = annotation_scheme.get('mode', 'closed') groups = annotation_scheme.get('groups', []) items_field = annotation_scheme.get('items_field', 'items') allow_empty_groups = annotation_scheme.get('allow_empty_groups', True) allow_multiple = annotation_scheme.get('allow_multiple', False) if mode == 'closed' and not groups: raise ValueError(f"card_sort schema '{schema_name}' in closed mode requires 'groups'") layout_attrs = generate_layout_attributes(annotation_scheme) validation = generate_validation_attribute(annotation_scheme) identifiers = generate_element_identifier(schema_name, schema_name, "hidden") config_json = json.dumps({ 'mode': mode, 'groups': groups, 'items_field': items_field, 'allow_empty_groups': allow_empty_groups, 'allow_multiple': allow_multiple, }) # Build group containers # NOTE: group-items div must be self-closing (no whitespace) so CSS :empty works groups_html = "" for group in groups: group_id = escape_html_content(group.replace(' ', '-').lower()) esc_group = escape_html_content(group) esc_schema = escape_html_content(schema_name) groups_html += ( f'