// Die Zeichnungen der Symbole für die Auswahl im Editor. Die Website zeichnet dieselben Pfade aus // templates/blocks/icon.html.twig; ein Test hält beide Listen und DocumentValidator::ICONS gleich. export const ICON_PATHS = { 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', 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', design: 'M4 20h16M6 16 18 4l3 3L9 19l-4 1Z', network: 'M5 3h14v5H5Zm0 13h14v5H5Zm7-8v8M8 8v3h8V8', hardware: 'M4 5h16v11H4Zm4 15h8m-4-4v4', 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', 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', 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', clock: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5.5l3.5 2', 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', 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', mail: 'M3 6h18v12H3Zm0 1 9 7 9-7', 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', check: 'm4 12.5 5.5 5.5L20 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', 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', home: 'm3 11.5 9-7.5 9 7.5M5.5 10v10h13V10M10 20v-6.5h4V20', '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-right': 'M6.5 17.5 17.5 6.5M8.5 6.5h9v9', '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', calendar: 'M4 6h16v14H4Zm0 5h16M8 3.5v4m8-4v4', chat: 'M4 5h16v11H9.5L5 19.5V16H4Z', send: 'M21 3 10 14M21 3l-6.5 18-4.5-7-7-4.5Z', download: 'M12 3.5v12m-5-5 5 5 5-5M4.5 20h15', document: 'M6 3h8l4 4v14H6Zm8 0v4h4M9 12h6m-6 4h6', play: 'M8 5.5v13l10-6.5Z', 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', 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', info: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 8v5.5m0-9v.5', plus: 'M12 5v14M5 12h14', 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', }; export const ICON_LABELS = { app: 'Smartphone', web: 'Globus', design: 'Stift', network: 'Netzwerk', hardware: 'Bildschirm', shop: 'Einkaufstasche', support: 'Kopfhörer', shield: 'Schutz', clock: 'Uhr', star: 'Stern', phone: 'Telefon', mail: 'E-Mail', pin: 'Ort', check: 'Häkchen', people: 'Personen', music: 'Musik', home: 'Haus', 'arrow-right': 'Pfeil nach rechts', 'arrow-left': 'Pfeil nach links', 'arrow-up-right': 'Pfeil schräg', 'arrow-down': 'Pfeil nach unten', 'chevron-right': 'Winkel', calendar: 'Kalender', chat: 'Nachricht', send: 'Senden', download: 'Download', document: 'Dokument', play: 'Abspielen', search: 'Suche', heart: 'Herz', info: 'Info', plus: 'Plus', user: 'Person', }; /** Die Auswahl am Button: die Symbole, die man an einem Knopf wirklich braucht, nach Zweck geordnet. */ export const BUTTON_ICON_GROUPS = [ ['Pfeile', ['arrow-up-right', 'arrow-right', 'chevron-right', 'arrow-down', 'arrow-left']], ['Kontakt', ['phone', 'mail', 'chat', 'send', 'calendar', 'pin']], ['Allgemein', ['download', 'document', 'play', 'search', 'check', 'star', 'heart', 'info', 'plus', 'user', 'home', 'shop', 'clock']], ]; /** Alle Symbole für Symbol und Symbolkarte, in derselben Ordnung plus die übrigen. */ export const ALL_ICON_GROUPS = [ ...BUTTON_ICON_GROUPS, ['Weitere', ['app', 'web', 'design', 'network', 'hardware', 'support', 'shield', 'people', 'music']], ]; 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'); const path = document.createElementNS(svgNS, 'path'); path.setAttribute('d', ICON_PATHS[name] ?? ''); svg.append(path); return svg; } /** * Eine Kachelauswahl statt einer Liste aus Namen: jedes Symbol ist ein Knopf mit seiner Zeichnung, * der gewählte ist markiert. Pfeiltasten wandern innerhalb der Gruppe, Tab verlässt die Auswahl. * * @param {{ label: string, value: string, groups: Array<[string, string[]]>, none?: string, onChoose: (name: string) => void }} options */ export function renderIconPicker({ label, value, groups, none, onChoose }) { const wrap = document.createElement('div'); wrap.className = 'icon-picker'; const heading = document.createElement('p'); heading.className = 'icon-picker-label'; heading.id = `icon-picker-${Math.random().toString(36).slice(2, 8)}`; heading.textContent = label; wrap.append(heading); const tiles = []; const tile = name => { const button = document.createElement('button'); button.type = 'button'; button.className = name === 'none' ? 'icon-tile icon-tile-none' : 'icon-tile'; button.dataset.icon = name; const text = name === 'none' ? none : ICON_LABELS[name]; button.title = text; button.setAttribute('aria-label', text); button.setAttribute('aria-pressed', String(name === value)); button.tabIndex = name === value ? 0 : -1; if (name === 'none') { const mark = document.createElement('span'); mark.className = 'icon-tile-empty'; mark.setAttribute('aria-hidden', 'true'); button.append(mark); } else button.append(iconSvg(name)); button.addEventListener('click', () => onChoose(name)); tiles.push(button); return button; }; const group = document.createElement('div'); group.setAttribute('role', 'group'); group.setAttribute('aria-labelledby', heading.id); if (none) { const row = document.createElement('div'); row.className = 'icon-grid'; row.append(tile('none')); group.append(row); } for (const [title, names] of groups) { const caption = document.createElement('p'); caption.className = 'icon-group-title'; caption.textContent = title; const grid = document.createElement('div'); grid.className = 'icon-grid'; for (const name of names) grid.append(tile(name)); group.append(caption, grid); } // Ohne gewählten Knopf in der Liste (etwa ein Symbol außerhalb der Gruppen) bleibt der erste // erreichbar, sonst käme man mit Tab gar nicht hinein. if (!tiles.some(button => button.tabIndex === 0)) tiles[0].tabIndex = 0; group.addEventListener('keydown', event => { const index = tiles.indexOf(document.activeElement); if (index < 0) return; const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key]; const target = event.key === 'Home' ? 0 : event.key === 'End' ? tiles.length - 1 : step ? (index + step + tiles.length) % tiles.length : null; if (target === null) return; event.preventDefault(); tiles[index].tabIndex = -1; tiles[target].tabIndex = 0; tiles[target].focus(); }); wrap.append(group); return wrap; }