The menu editor gets "Als Button darstellen" per entry: shape, appearance and size from the button design, fill and ink with hover and palette, icon colour, icon from the dialog, motion, icon-only and where the button sits on the phone (in the opened menu, beside the burger, or there as an icon only). Colours go into the page stylesheet under a stable per-button id because the CSP forbids inline styles. ADR 110. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
281 lines
18 KiB
JavaScript
281 lines
18 KiB
JavaScript
// 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',
|
|
};
|
|
|
|
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',
|
|
};
|
|
|
|
/** 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']],
|
|
];
|
|
|
|
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;
|
|
}
|