// Der Symbolsatz: Zeichnung, deutscher Name und Kategorie. Die Website zeichnet dieselben Pfade aus // templates/blocks/icon.html.twig; ein Test hält Twig, diese Datei und DocumentValidator::ICONS gleich. // Alle Symbole sind Linien auf einem 24er-Raster (stroke, keine Füllung) und erben die Textfarbe. export const ICON_PATHS = { // Pfeile und Navigation 'arrow-up-right': 'M6.5 17.5 17.5 6.5M8.5 6.5h9v9', 'arrow-right': 'M4 12h15.5m-6.5-6.5 6.5 6.5-6.5 6.5', 'arrow-left': 'M20 12H4.5M11 5.5 4.5 12 11 18.5', 'arrow-up': 'M12 20V4.5m-6.5 6.5L12 4.5l6.5 6.5', 'arrow-down': 'M12 4v15.5m-6.5-6.5 6.5 6.5 6.5-6.5', 'chevron-right': 'm9 5.5 6.5 6.5L9 18.5', 'chevron-left': 'M15 5.5 8.5 12l6.5 6.5', 'chevron-up': 'm5.5 15 6.5-6.5 6.5 6.5', 'chevron-down': 'm5.5 9 6.5 6.5L18.5 9', 'arrow-circle': 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9h8m-3-3 3 3-3 3', external: 'M14 4h6v6m0-6-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5', refresh: 'M20 12a8 8 0 1 1-2.3-5.6M20 4v5h-5', menu: 'M4 7h16M4 12h16M4 17h16', close: 'M6 6l12 12M18 6 6 18', plus: 'M12 5v14M5 12h14', minus: 'M5 12h14', // Kontakt und Ort phone: 'M6 3h4l2 5-2.5 1.5a12 12 0 0 0 5 5L16 12l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 4 5a2 2 0 0 1 2-2Z', app: 'M7 2h10a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Zm4 17h2', mail: 'M3 6h18v12H3Zm0 1 9 7 9-7', chat: 'M4 5h16v11H9.5L5 19.5V16H4Z', send: 'M21 3 10 14M21 3l-6.5 18-4.5-7-7-4.5Z', calendar: 'M4 6h16v14H4Zm0 5h16M8 3.5v4m8-4v4', clock: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5.5l3.5 2', pin: 'M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z', map: 'M9 4 3 6.5V20l6-2.5 6 2.5 6-2.5V4l-6 2.5ZM9 4v13.5m6-11V20', navigation: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm3.5 5.5-2 5-5 2 2-5Z', web: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 0c2.5 2.4 3.8 5.4 3.8 9s-1.3 6.6-3.8 9m0-18C9.5 5.4 8.2 8.4 8.2 12s1.3 6.6 3.8 9M3.2 9h17.6M3.2 15h17.6', wifi: 'M3 9.5a13 13 0 0 1 18 0M6 13a8 8 0 0 1 12 0m-9 3.5a3.5 3.5 0 0 1 6 0M12 19.5h.01', support: 'M12 3a7 7 0 0 0-7 7v5m14-5a7 7 0 0 0-7-7m7 7v5m-14 0a2 2 0 0 0 2 2h1v-5H5Zm14 0a2 2 0 0 1-2 2h-1v-5h3Z', // Handel und Geld shop: 'M4 7h16l-1.4 11.2a2 2 0 0 1-2 1.8H7.4a2 2 0 0 1-2-1.8Zm4 0a4 4 0 0 1 8 0', cart: 'M3 4h2.5l2.2 10.5h10.6L20.5 7H6.3M9.5 18a1.2 1.2 0 1 0 0 2.4 1.2 1.2 0 0 0 0-2.4Zm7.5 0a1.2 1.2 0 1 0 0 2.4 1.2 1.2 0 0 0 0-2.4Z', tag: 'M3.5 12.5v-8a1 1 0 0 1 1-1h8l8 8a1.4 1.4 0 0 1 0 2l-7 7a1.4 1.4 0 0 1-2 0ZM8.5 8h.01', euro: 'M17.5 6.5A6.5 6.5 0 1 0 17.5 17.5M4 10h9m-9 4h9', 'credit-card': 'M3 6h18v12H3Zm0 4h18M6.5 15h3', gift: 'M4 9h16v4H4Zm1.5 4v7h13v-7M12 9v11m0-11S10.5 4 8 4.5 7.5 9 12 9Zm0 0s1.5-5 4-4.5S16.5 9 12 9Z', percent: 'M19 5 5 19M7 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm10 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', truck: 'M2.5 6h11v10.5h-11Zm11 3.5h4l3 3.5v3.5h-7ZM6.5 17a1.8 1.8 0 1 0 0 3.6 1.8 1.8 0 0 0 0-3.6Zm10.5 0a1.8 1.8 0 1 0 0 3.6 1.8 1.8 0 0 0 0-3.6Z', package: 'M3.5 7.5 12 3l8.5 4.5v9L12 21l-8.5-4.5Zm0 0L12 12l8.5-4.5M12 12v9M7.8 5.2l8.5 4.6', briefcase: 'M3.5 7.5h17v12h-17Zm5-3h7v3h-7Zm-5 8h17', // Dateien und Medien document: 'M6 3h8l4 4v14H6Zm8 0v4h4M9 12h6m-6 4h6', download: 'M12 3.5v12m-5-5 5 5 5-5M4.5 20h15', upload: 'M12 15.5V4m-5 5 5-5 5 5M4.5 20h15', print: 'M7 9V3.5h10V9M7 17H4.5V9h15v8H17M7 14h10v6.5H7Z', link: 'M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1.2 1.2M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1.2-1.2', share: 'M18 3.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM6 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Zm12 6a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM8.2 10.8l7.6-4.1m-7.6 6.5 7.6 4.1', image: 'M4 4h16v16H4Zm0 12 4.5-4.5 3.5 3.5 2.5-2.5L20 18M15 8.5h.01', camera: 'M3 7.5h4L8.5 5h7L17 7.5h4V19H3Zm9 2.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z', video: 'M3 6.5h12v11H3Zm12 4 6-3.5v10l-6-3.5', play: 'M8 5.5v13l10-6.5Z', pause: 'M8 5v14m8-14v14', music: 'M10 18V5.5L20 3v12.5M10 9.5 20 7M10 18a3 2.5 0 1 0-6 0 3 2.5 0 0 0 6 0ZM20 15.5a3 2.5 0 1 0-6 0 3 2.5 0 0 0 6 0Z', mic: 'M12 3a3 3 0 0 0-3 3v5.5a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3ZM5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21', book: 'M5 4.5A1.5 1.5 0 0 1 6.5 3H19v15H6.5A1.5 1.5 0 0 0 5 19.5Zm0 15A1.5 1.5 0 0 0 6.5 21H19v-3', // Personen und Sicherheit user: 'M12 11.5a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM4.5 20.5c0-3.9 3.4-6 7.5-6s7.5 2.1 7.5 6', people: 'M9.5 11.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7ZM2.5 20v-1c0-3.3 3-5 7-5s7 1.7 7 5v1M16.5 5.3a3.2 3.2 0 0 1 0 6.4M18 14.4c2.1.7 3.5 2.1 3.5 4.1V20', login: 'M14 4h5a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-5M3.5 12h11m-4-4 4 4-4 4', lock: 'M5 10.5h14V21H5Zm3 0V7a4 4 0 0 1 8 0v3.5M12 14.5v3', key: 'M7.5 11.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm2.8 1.2L19 4m-3 3 2.5 2.5M13 10l2 2', shield: 'M12 3 5 6v6c0 4.4 2.9 7.9 7 9 4.1-1.1 7-4.6 7-9V6Zm-2.5 9 2 2 4-4.5', eye: 'M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Zm9.5-3a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z', 'thumbs-up': 'M7 10.5V20H4v-9.5Zm0 0 3.5-7a2 2 0 0 1 2.5 2v3.5h5.2a2 2 0 0 1 2 2.4l-1.4 7a2 2 0 0 1-2 1.6H7', award: 'M12 3a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11Zm-3.5 10L7 21l5-2.5 5 2.5-1.5-8', graduation: 'M2.5 9 12 4.5 21.5 9 12 13.5Zm4 2.3V16c0 1.5 2.5 3 5.5 3s5.5-1.5 5.5-3v-4.7M21.5 9v5', heart: 'M12 20s-7.5-4.6-7.5-10A4.3 4.3 0 0 1 12 7.4 4.3 4.3 0 0 1 19.5 10c0 5.4-7.5 10-7.5 10Z', star: 'm12 3 2.7 5.9 6.3.7-4.7 4.3 1.3 6.1L12 17l-5.6 3 1.3-6.1L3 9.6l6.3-.7Z', // Haus, Garten und Handwerk home: 'm3 11.5 9-7.5 9 7.5M5.5 10v10h13V10M10 20v-6.5h4V20', building: 'M5 21V4h10v17m0-12h4v12M3 21h18M8.5 7.5h3m-3 3.5h3m-3 3.5h3', wrench: 'M14.5 3.5a5 5 0 0 0-4.6 6.9L3.5 16.8a1.9 1.9 0 0 0 2.7 2.7l6.4-6.4a5 5 0 0 0 6.9-4.6l-3 3-3-1-1-3Z', hammer: 'M14 8.5 4.5 18a1.5 1.5 0 0 0 2.1 2.1L16 10.5M11 6l3-3 3.5 1L21 7.5 18.5 10l-2-2-2.5 2.5Z', brush: 'M4 4h13v5H4Zm13 2.5h2.5V12h-8v3M10 15h3v6h-3Z', ruler: 'M3.5 16 16 3.5 20.5 8 8 20.5ZM7 13l2 2m1-5 2 2m1-5 2 2', leaf: 'M5 19C5 10 10 5 20 4c-1 10-6 15-15 15Zm0 0 8-8', tree: 'M12 21v-5m0-13-6 8h3l-4 5h14l-4-5h3Z', sun: 'M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0-5v2m0 14v2M3 12h2m14 0h2M5.6 5.6 7 7m10 10 1.4 1.4M5.6 18.4 7 17M17 7l1.4-1.4', droplet: 'M12 3s6.5 7 6.5 11.5a6.5 6.5 0 0 1-13 0C5.5 10 12 3 12 3Z', flame: 'M12 21a6 6 0 0 0 6-6c0-4.5-4-6.5-4.5-11-2.5 2-5 5-5 8.5-1-.5-1.5-1.5-1.8-2.5C5.6 11.5 6 13 6 15a6 6 0 0 0 6 6Z', bolt: 'M13 2.5 4.5 13.5H12l-1 8 8.5-11H12Z', solar: 'M5 4h15l-2 9.5H3Zm-1 4.75h15M10 4 8.5 13.5M15.5 4 14 13.5M10.5 13.5V18M7 20.5h7', thermometer: 'M10 14.5V5a2 2 0 0 1 4 0v9.5a4 4 0 1 1-4 0Z', snowflake: 'M12 2.5v19M3.8 7.3l16.4 9.4M3.8 16.7l16.4-9.4M9.5 4 12 6l2.5-2m-5 16 2.5-2 2.5 2', car: 'M5 16.5V12l2-5h10l2 5v4.5M3 12h18v4.5H3Zm3.5 4.5V19m11-2.5V19M7 14h.01M17 14h.01', // Allgemein check: 'm4 12.5 5.5 5.5L20 6', 'check-circle': 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-4 9.2 2.8 2.8L16.5 9', info: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 8v5.5m0-9v.5', help: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6v.6m0 3v.01', alert: 'M12 3.5 2.5 20h19Zm0 6V14m0 3v.01', search: 'M10.5 4a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm4.8 11.3L20 20', settings: 'M4 7h9m4 0h3M4 17h3m4 0h9M15 5v4M9 15v4', filter: 'M3.5 5h17l-6.5 8v6l-4 2v-8Z', grid: 'M4 4h7v7H4Zm9 0h7v7h-7ZM4 13h7v7H4Zm9 0h7v7h-7Z', list: 'M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01', sparkle: 'M12 3c.6 4.4 2.6 6.4 7 7-4.4.6-6.4 2.6-7 7-.6-4.4-2.6-6.4-7-7 4.4-.6 6.4-2.6 7-7Zm6.5 12.5c.2 1.6 1 2.3 2.5 2.5-1.5.2-2.3 1-2.5 2.5-.2-1.5-1-2.3-2.5-2.5 1.5-.2 2.3-1 2.5-2.5Z', coffee: 'M4 9h12v6a5 5 0 0 1-5 5H9a5 5 0 0 1-5-5Zm12 1.5h1.5a2.5 2.5 0 0 1 0 5H16M8 3.5V6m4-2.5V6', utensils: 'M6 3v7a2 2 0 0 0 4 0V3M8 10v11m8.5 0V3c-2 1-3 3.5-3 6.5v4h3', laptop: 'M5 5h14v10H5Zm-2.5 10h19l-1 3.5h-17Z', hardware: 'M4 5h16v11H4Zm4 15h8m-4-4v4', network: 'M5 3h14v5H5Zm0 13h14v5H5Zm7-8v8M8 8v3h8V8', design: 'M4 20h16M6 16 18 4l3 3L9 19l-4 1Z', // Soziale Netzwerke facebook: 'M15.5 4h-2.2A3.8 3.8 0 0 0 9.5 7.8V10H7v3.2h2.5V20h3.3v-6.8h2.4l.5-3.2h-2.9V8.2c0-.6.3-.9.9-.9h2Z', instagram: 'M8.1 3.5h7.8a4.6 4.6 0 0 1 4.6 4.6v7.8a4.6 4.6 0 0 1-4.6 4.6H8.1a4.6 4.6 0 0 1-4.6-4.6V8.1a4.6 4.6 0 0 1 4.6-4.6ZM12 8.1a3.9 3.9 0 1 0 0 7.8 3.9 3.9 0 0 0 0-7.8ZM17 7h.01', whatsapp: 'M4 20l1.4-4A8.5 8.5 0 1 1 8.6 19.1ZM9.3 9.2c0 3 2.5 5.5 5.5 5.5l1.3-1.4-2-.9-.9.8a4.1 4.1 0 0 1-2.3-2.3l.8-.9-.9-2Z', }; export const ICON_LABELS = { 'arrow-up-right': 'Pfeil schräg', 'arrow-right': 'Pfeil nach rechts', 'arrow-left': 'Pfeil nach links', 'arrow-up': 'Pfeil nach oben', 'arrow-down': 'Pfeil nach unten', 'chevron-right': 'Winkel rechts', 'chevron-left': 'Winkel links', 'chevron-up': 'Winkel oben', 'chevron-down': 'Winkel unten', 'arrow-circle': 'Pfeil im Kreis', external: 'Externer Link', refresh: 'Aktualisieren', menu: 'Menü', close: 'Schließen', plus: 'Plus', minus: 'Minus', phone: 'Telefon', app: 'Smartphone', mail: 'E-Mail', chat: 'Nachricht', send: 'Senden', calendar: 'Kalender', clock: 'Uhr', pin: 'Ort', map: 'Karte', navigation: 'Navigation', web: 'Globus', wifi: 'WLAN', support: 'Kundendienst', shop: 'Einkaufstasche', cart: 'Warenkorb', tag: 'Preisschild', euro: 'Euro', 'credit-card': 'Karte bezahlen', gift: 'Geschenk', percent: 'Prozent', truck: 'Lieferung', package: 'Paket', briefcase: 'Aktentasche', document: 'Dokument', download: 'Download', upload: 'Hochladen', print: 'Drucken', link: 'Link', share: 'Teilen', image: 'Bild', camera: 'Kamera', video: 'Video', play: 'Abspielen', pause: 'Pause', music: 'Musik', mic: 'Mikrofon', book: 'Buch', user: 'Person', people: 'Personen', login: 'Anmelden', lock: 'Schloss', key: 'Schlüssel', shield: 'Schutz', eye: 'Ansehen', 'thumbs-up': 'Daumen hoch', award: 'Auszeichnung', graduation: 'Ausbildung', heart: 'Herz', star: 'Stern', home: 'Haus', building: 'Gebäude', wrench: 'Werkzeug', hammer: 'Hammer', brush: 'Malerrolle', ruler: 'Lineal', leaf: 'Blatt', tree: 'Baum', sun: 'Sonne', droplet: 'Wasser', flame: 'Flamme', bolt: 'Strom', solar: 'Solar', thermometer: 'Temperatur', snowflake: 'Kälte', car: 'Auto', check: 'Häkchen', 'check-circle': 'Häkchen im Kreis', info: 'Info', help: 'Frage', alert: 'Warnung', search: 'Suche', settings: 'Einstellungen', filter: 'Filter', grid: 'Raster', list: 'Liste', sparkle: 'Funkeln', coffee: 'Kaffee', utensils: 'Besteck', laptop: 'Laptop', hardware: 'Bildschirm', network: 'Netzwerk', design: 'Stift', facebook: 'Facebook', instagram: 'Instagram', whatsapp: 'WhatsApp', }; /** Die Kategorien des Auswahlfensters, in der Reihenfolge der Reiter. Jedes Symbol steht genau einmal. */ export const ICON_GROUPS = [ ['Pfeile', ['arrow-up-right', 'arrow-right', 'arrow-left', 'arrow-up', 'arrow-down', 'chevron-right', 'chevron-left', 'chevron-up', 'chevron-down', 'arrow-circle', 'external', 'refresh', 'menu', 'close', 'plus', 'minus']], ['Kontakt & Ort', ['phone', 'app', 'mail', 'chat', 'send', 'calendar', 'clock', 'pin', 'map', 'navigation', 'web', 'wifi', 'support']], ['Handel & Geld', ['shop', 'cart', 'tag', 'euro', 'credit-card', 'gift', 'percent', 'truck', 'package', 'briefcase']], ['Dateien & Medien', ['document', 'download', 'upload', 'print', 'link', 'share', 'image', 'camera', 'video', 'play', 'pause', 'music', 'mic', 'book']], ['Personen & Sicherheit', ['user', 'people', 'login', 'lock', 'key', 'shield', 'eye', 'thumbs-up', 'award', 'graduation', 'heart', 'star']], ['Haus & Handwerk', ['home', 'building', 'wrench', 'hammer', 'brush', 'ruler', 'leaf', 'tree', 'sun', 'droplet', 'flame', 'bolt', 'solar', 'thermometer', 'snowflake', 'car']], ['Allgemein', ['check', 'check-circle', 'info', 'help', 'alert', 'search', 'settings', 'filter', 'grid', 'list', 'sparkle', 'coffee', 'utensils', 'laptop', 'hardware', 'network', 'design']], ['Soziale Netzwerke', ['facebook', 'instagram', 'whatsapp']], ]; const svgNS = 'http://www.w3.org/2000/svg'; export function iconSvg(name) { const svg = document.createElementNS(svgNS, 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true'); svg.setAttribute('focusable', 'false'); // Wie auf der Website: Linien in der Textfarbe. CSS darf Größe und Strichstärke überschreiben. for (const [name, value] of Object.entries({ fill: 'none', stroke: 'currentColor', 'stroke-width': '1.6', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' })) svg.setAttribute(name, value); const path = document.createElementNS(svgNS, 'path'); path.setAttribute('d', ICON_PATHS[name] ?? ''); svg.append(path); return svg; } const element = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; }; /** * Das Feld im Panel: das gewählte Symbol als Vorschau mit Namen und ein Knopf, der das * Auswahlfenster öffnet. Die Kacheln stehen nicht im Panel - bei hundert Symbolen wäre es eine * lange Wand, die alles andere nach unten drückt. * * @param {{ label: string, value: string, none?: string, onChoose: (name: string) => void }} options */ export function renderIconField({ label, value, none, onChoose }) { const wrap = element('div', 'icon-field'); wrap.append(element('p', 'icon-picker-label', label)); const row = element('div', 'icon-field-row'); const preview = element('span', 'icon-field-preview'); preview.setAttribute('aria-hidden', 'true'); if (value && value !== 'none') preview.append(iconSvg(value)); else preview.classList.add('is-empty'); const name = element('span', 'icon-field-name', value && value !== 'none' ? ICON_LABELS[value] ?? value : none ?? 'Kein Symbol'); const open = element('button', 'secondary icon-field-change', 'Ändern …'); open.type = 'button'; open.setAttribute('aria-haspopup', 'dialog'); open.setAttribute('aria-label', `${label} ändern, aktuell: ${name.textContent}`); open.addEventListener('click', () => openIconDialog({ value, none, onChoose, returnFocus: open })); row.append(preview, name, open); wrap.append(row); return wrap; } /** * Das Auswahlfenster: Kategorien als Reiter links, die Symbole einer Kategorie als Kacheln mit * Namen. Ein Klick wählt und schließt, Escape schließt ohne Änderung. Geöffnet wird bei der * Kategorie des gewählten Symbols, der Fokus steht auf ihm. */ export function openIconDialog({ value, none, onChoose, returnFocus }) { const dialog = element('dialog', 'icon-dialog'); dialog.setAttribute('aria-labelledby', 'icon-dialog-title'); const head = element('div', 'icon-dialog-head'); head.append(element('h2', null, 'Symbol wählen')); head.firstChild.id = 'icon-dialog-title'; const close = element('button', 'icon-dialog-close'); close.type = 'button'; close.setAttribute('aria-label', 'Schließen'); close.append(iconSvg('close')); close.addEventListener('click', () => dialog.close()); head.append(close); const body = element('div', 'icon-dialog-body'); const tablist = element('div', 'icon-dialog-tabs'); tablist.setAttribute('role', 'tablist'); tablist.setAttribute('aria-orientation', 'vertical'); tablist.setAttribute('aria-label', 'Kategorien'); const panel = element('div', 'icon-dialog-panel'); panel.setAttribute('role', 'tabpanel'); panel.id = 'icon-dialog-panel'; const tabs = []; let tiles = []; const choose = name => { dialog.close(); onChoose(name); }; const show = (index, focusTile = false) => { tabs.forEach((tab, i) => { tab.setAttribute('aria-selected', String(i === index)); tab.tabIndex = i === index ? 0 : -1; }); panel.setAttribute('aria-labelledby', tabs[index].id); panel.replaceChildren(); const grid = element('div', 'icon-dialog-grid'); tiles = ICON_GROUPS[index][1].map(name => { const tile = element('button', 'icon-choice'); tile.type = 'button'; tile.dataset.icon = name; tile.setAttribute('aria-pressed', String(name === value)); tile.tabIndex = -1; tile.append(iconSvg(name), element('span', 'icon-choice-name', ICON_LABELS[name])); tile.addEventListener('click', () => choose(name)); grid.append(tile); return tile; }); (tiles.find(tile => tile.dataset.icon === value) ?? tiles[0]).tabIndex = 0; panel.append(grid); if (focusTile) (tiles.find(tile => tile.tabIndex === 0)).focus(); }; ICON_GROUPS.forEach(([title], index) => { const tab = element('button', 'icon-dialog-tab', title); tab.type = 'button'; tab.id = `icon-tab-${index}`; tab.setAttribute('role', 'tab'); tab.setAttribute('aria-controls', panel.id); tab.addEventListener('click', () => show(index)); tabs.push(tab); tablist.append(tab); }); tablist.addEventListener('keydown', event => { const index = tabs.indexOf(document.activeElement); const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key]; if (index < 0 || !step) return; event.preventDefault(); const next = (index + step + tabs.length) % tabs.length; show(next); tabs[next].focus(); }); panel.addEventListener('keydown', event => { const index = tiles.indexOf(document.activeElement); if (index < 0) return; const columns = Math.max(1, Math.round(panel.querySelector('.icon-dialog-grid').clientWidth / tiles[0].offsetWidth)); const step = { ArrowRight: 1, ArrowLeft: -1, ArrowDown: columns, ArrowUp: -columns }[event.key]; const target = event.key === 'Home' ? 0 : event.key === 'End' ? tiles.length - 1 : step ? Math.min(tiles.length - 1, Math.max(0, index + step)) : null; if (target === null) return; event.preventDefault(); tiles[index].tabIndex = -1; tiles[target].tabIndex = 0; tiles[target].focus(); }); body.append(tablist, panel); const foot = element('div', 'icon-dialog-foot'); if (none) { const without = element('button', 'secondary', none); without.type = 'button'; without.dataset.icon = 'none'; without.setAttribute('aria-pressed', String(value === 'none')); without.addEventListener('click', () => choose('none')); foot.append(without); } const cancel = element('button', 'text-button', 'Abbrechen'); cancel.type = 'button'; cancel.addEventListener('click', () => dialog.close()); foot.append(cancel); dialog.append(head, body, foot); dialog.addEventListener('close', () => { dialog.remove(); if (returnFocus?.isConnected) returnFocus.focus(); }); // Ein Klick auf den abgedunkelten Rand schließt, wie bei den anderen Fenstern des Editors. dialog.addEventListener('click', event => { if (event.target === dialog) dialog.close(); }); document.body.append(dialog); const start = Math.max(0, ICON_GROUPS.findIndex(([, names]) => names.includes(value))); dialog.showModal(); show(start, true); return dialog; }