Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5
9 changed files with 432 additions and 42 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.79-beta
|
0.3.80-beta
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@
|
||||||
.image-catalog-modal .gallery-head{position:sticky;top:-24px;z-index:4;background:var(--surface);padding:4px 0 8px;margin-top:-4px}
|
.image-catalog-modal .gallery-head{position:sticky;top:-24px;z-index:4;background:var(--surface);padding:4px 0 8px;margin-top:-4px}
|
||||||
.ic-tools{display:grid;grid-template-columns:minmax(180px,1.5fr) repeat(3,minmax(130px,.8fr)) auto;gap:8px;position:sticky;top:12px;z-index:3;background:var(--surface);padding:8px 0 10px}
|
.ic-tools{display:grid;grid-template-columns:minmax(180px,1.5fr) repeat(3,minmax(130px,.8fr)) auto;gap:8px;position:sticky;top:12px;z-index:3;background:var(--surface);padding:8px 0 10px}
|
||||||
.ic-tools input,.ic-tools select,.ic-add select,.ic-add input,.ic-editor input,.ic-editor select{min-width:0;width:100%;border:1px solid var(--line);border-radius:10px;padding:9px 10px;background:var(--surface);color:var(--ink);font:inherit}
|
.ic-tools input,.ic-tools select,.ic-add select,.ic-add input,.ic-editor input,.ic-editor select{min-width:0;width:100%;border:1px solid var(--line);border-radius:10px;padding:9px 10px;background:var(--surface);color:var(--ink);font:inherit}
|
||||||
|
.ic-word-themes{display:flex;align-items:center;gap:7px;overflow-x:auto;padding:9px 10px;margin:0 0 10px;border:1px solid var(--line);border-radius:13px;background:linear-gradient(135deg,var(--surface-2),var(--accent-soft));scrollbar-width:thin}
|
||||||
|
.ic-word-themes strong{flex:none;margin-right:3px;font-size:12px;color:var(--muted)}
|
||||||
|
.ic-word-themes button{flex:none;border:1px solid var(--line);border-radius:999px;padding:7px 11px;background:var(--surface);color:var(--ink);font:inherit;font-size:12px;font-weight:850;cursor:pointer;transition:transform .12s,border-color .12s,background .12s}
|
||||||
|
.ic-word-themes button:hover{transform:translateY(-1px);border-color:var(--accent)}
|
||||||
|
.ic-word-themes button.active{border-color:var(--accent);background:var(--accent);color:#fff;box-shadow:var(--shadow-1)}
|
||||||
.ic-quota{padding:8px 12px;margin-bottom:10px;border-radius:10px;background:var(--accent-soft);color:var(--accent-ink);font-size:13px;font-weight:800}
|
.ic-quota{padding:8px 12px;margin-bottom:10px;border-radius:10px;background:var(--accent-soft);color:var(--accent-ink);font-size:13px;font-weight:800}
|
||||||
.ic-add{border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-bottom:10px;background:var(--surface-2)}
|
.ic-add{border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-bottom:10px;background:var(--surface-2)}
|
||||||
.ic-add summary{cursor:pointer;font-weight:900}
|
.ic-add summary{cursor:pointer;font-weight:900}
|
||||||
|
|
@ -31,6 +36,7 @@ body.hc .ic-card,body.hc .ic-add,body.hc .ic-tools input,body.hc .ic-tools selec
|
||||||
.image-catalog-modal{width:100vw;max-height:100dvh;height:100dvh;border-radius:0;padding:16px}
|
.image-catalog-modal{width:100vw;max-height:100dvh;height:100dvh;border-radius:0;padding:16px}
|
||||||
.image-catalog-modal .gallery-head{top:-16px}
|
.image-catalog-modal .gallery-head{top:-16px}
|
||||||
.ic-tools{top:20px;grid-template-columns:1fr 1fr;padding-top:4px}.ic-search,.ic-folders{grid-column:1/-1}
|
.ic-tools{top:20px;grid-template-columns:1fr 1fr;padding-top:4px}.ic-search,.ic-folders{grid-column:1/-1}
|
||||||
|
.ic-word-themes{margin-inline:-2px;padding:8px}.ic-word-themes strong{position:sticky;left:0;z-index:1;padding:7px 6px;background:var(--surface-2)}
|
||||||
.ic-add-grid,.ic-editor-grid{grid-template-columns:1fr}.ic-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
|
.ic-add-grid,.ic-editor-grid{grid-template-columns:1fr}.ic-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||||
.ic-choose{grid-template-rows:96px auto auto}.ic-choose img{height:96px}
|
.ic-choose{grid-template-rows:96px auto auto}.ic-choose img{height:96px}
|
||||||
.img-quota-row{grid-template-columns:1fr 1fr}.img-quota-row .am-name{grid-column:1/-1}
|
.img-quota-row{grid-template-columns:1fr 1fr}.img-quota-row .am-name{grid-column:1/-1}
|
||||||
|
|
|
||||||
|
|
@ -813,11 +813,12 @@
|
||||||
en de accentlijn van de actieve tab valt daaroverheen. */
|
en de accentlijn van de actieve tab valt daaroverheen. */
|
||||||
.ed-tabs{
|
.ed-tabs{
|
||||||
display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:14px;
|
display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:14px;
|
||||||
|
overflow-x:auto; scrollbar-width:thin;
|
||||||
}
|
}
|
||||||
.ed-tabs button{
|
.ed-tabs button{
|
||||||
border:none; border-radius:0; padding:10px 15px 11px; font:inherit; font-weight:700;
|
border:none; border-radius:0; padding:10px 15px 11px; font:inherit; font-weight:700;
|
||||||
font-size:14px; background:transparent; color:var(--muted); cursor:pointer;
|
font-size:14px; background:transparent; color:var(--muted); cursor:pointer;
|
||||||
position:relative; margin-bottom:-1px;
|
position:relative; margin-bottom:-1px; flex:none;
|
||||||
}
|
}
|
||||||
.ed-tabs button:hover{color:var(--ink);}
|
.ed-tabs button:hover{color:var(--ink);}
|
||||||
.ed-tabs button.active{color:var(--ink);}
|
.ed-tabs button.active{color:var(--ink);}
|
||||||
|
|
@ -858,6 +859,9 @@
|
||||||
font-size:14px; cursor:pointer; display:inline-block;}
|
font-size:14px; cursor:pointer; display:inline-block;}
|
||||||
.ed-upload input{display:none;}
|
.ed-upload input{display:none;}
|
||||||
.ed-uphint{font-size:12.5px; color:var(--muted); margin-top:10px; line-height:1.45;}
|
.ed-uphint{font-size:12.5px; color:var(--muted); margin-top:10px; line-height:1.45;}
|
||||||
|
.ed-p-library{min-height:120px;padding:18px;border:1px dashed var(--accent);border-radius:14px;background:linear-gradient(135deg,var(--surface-2),var(--accent-soft));text-align:center;}
|
||||||
|
.ed-open-library{min-height:46px;font-weight:850;}
|
||||||
|
.ed-library-hint{max-width:520px;margin:10px auto 0;color:var(--muted);font-size:13px;line-height:1.5;}
|
||||||
.lg-ed-list{display:flex; flex-direction:column; gap:6px;}
|
.lg-ed-list{display:flex; flex-direction:column; gap:6px;}
|
||||||
.lg-ed-list .empty{color:var(--muted); font-weight:700; text-align:center; padding:18px 0;}
|
.lg-ed-list .empty{color:var(--muted); font-weight:700; text-align:center; padding:18px 0;}
|
||||||
.ed-row{display:flex; align-items:center; gap:12px; background:var(--surface-2); border-radius:12px; padding:7px 12px;}
|
.ed-row{display:flex; align-items:center; gap:12px; background:var(--surface-2); border-radius:12px; padding:7px 12px;}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
||||||
Kept in sync by hand with the VERSION file at the repo root on every release. */
|
Kept in sync by hand with the VERSION file at the repo root on every release. */
|
||||||
const VERSION = "0.3.79-beta";
|
const VERSION = "0.3.80-beta";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
@ -48,7 +48,7 @@ const I18N = {
|
||||||
instr2_mid:"Zoek de <b>middelste letter</b> van het woord.",
|
instr2_mid:"Zoek de <b>middelste letter</b> van het woord.",
|
||||||
instr2_end:"Zoek de <b>eindletter</b> van het woord.",
|
instr2_end:"Zoek de <b>eindletter</b> van het woord.",
|
||||||
instr3:"Spel het woord bij het plaatje. De nopjes helpen je!",
|
instr3:"Spel het woord bij het plaatje. De nopjes helpen je!",
|
||||||
card:"Kaart", cardGeneral:"Algemeen", word:"Woord", welldone:"Goed zo!", next:"Volgende woord ▸",
|
card:"Kaart", cardGeneral:"Algemeen", cardStart:"Startwoorden", wordThemes:"Woordthema’s", word:"Woord", welldone:"Goed zo!", next:"Volgende woord ▸",
|
||||||
cardDone:"Kaart klaar! 🎉", pickCard:"Kies een nieuwe kaart of een ander niveau.",
|
cardDone:"Kaart klaar! 🎉", pickCard:"Kies een nieuwe kaart of een ander niveau.",
|
||||||
myWords:"Mijn woorden", manage:"Thema's en woorden beheren", addWord:"+ Toevoegen",
|
myWords:"Mijn woorden", manage:"Thema's en woorden beheren", addWord:"+ Toevoegen",
|
||||||
pickPic:"Kies een plaatje",
|
pickPic:"Kies een plaatje",
|
||||||
|
|
@ -167,7 +167,7 @@ const I18N = {
|
||||||
wordPh:"woord (2–5 letters)", close:"Sluiten",
|
wordPh:"woord (2–5 letters)", close:"Sluiten",
|
||||||
badWord:"Gebruik 2 t/m 5 letters (a-z).", needPic:"Kies eerst een plaatje.",
|
badWord:"Gebruik 2 t/m 5 letters (a-z).", needPic:"Kies eerst een plaatje.",
|
||||||
noWords:"Nog geen eigen woorden. Voeg er hierboven een toe!",
|
noWords:"Nog geen eigen woorden. Voeg er hierboven een toe!",
|
||||||
tabEmoji:"😀 Emoji", tabPicto:"🖼️ Picto's", tabUpload:"📷 Upload",
|
tabEmoji:"😀 Emoji", tabPicto:"🖼️ Picto's", tabUpload:"📷 Upload", tabLibrary:"🗂 Bibliotheek",
|
||||||
pictoSearch:"Zoeken", pictoPh:"zoekwoord…", pictoNone:"Niets gevonden. Probeer een ander woord.",
|
pictoSearch:"Zoeken", pictoPh:"zoekwoord…", pictoNone:"Niets gevonden. Probeer een ander woord.",
|
||||||
pictoLoad:"Zoeken…", pictoErr:"Kon picto's niet laden (internet nodig).",
|
pictoLoad:"Zoeken…", pictoErr:"Kon picto's niet laden (internet nodig).",
|
||||||
pictoCredit:"Picto's: <a href='https://arasaac.org' target='_blank'>ARASAAC</a> (CC BY-NC-SA, Gobierno de Aragón). Ook leuk: <a href='https://www.sclera.be' target='_blank'>sclera.be</a> (gratis download, dan via Upload toevoegen).",
|
pictoCredit:"Picto's: <a href='https://arasaac.org' target='_blank'>ARASAAC</a> (CC BY-NC-SA, Gobierno de Aragón). Ook leuk: <a href='https://www.sclera.be' target='_blank'>sclera.be</a> (gratis download, dan via Upload toevoegen).",
|
||||||
|
|
@ -175,6 +175,8 @@ const I18N = {
|
||||||
upHint:"Tip: vierkant plaatje, witte of doorzichtige achtergrond, één duidelijk voorwerp, géén tekst in het plaatje.",
|
upHint:"Tip: vierkant plaatje, witte of doorzichtige achtergrond, één duidelijk voorwerp, géén tekst in het plaatje.",
|
||||||
save:"Opslaan", cancel:"Annuleren",
|
save:"Opslaan", cancel:"Annuleren",
|
||||||
imgCatalog:"Afbeeldingscatalogus", imgSearch:"Zoek op naam, map of thema…",
|
imgCatalog:"Afbeeldingscatalogus", imgSearch:"Zoek op naam, map of thema…",
|
||||||
|
imgOpenLibrary:"Open afbeeldingenbibliotheek", imgLanguageHint:"Hier vind je alle vaste woordplaatjes, eigen woordkaarten en uploads bij elkaar.",
|
||||||
|
imgWordThemes:"Taalthema’s",
|
||||||
imgFolders:"Afbeeldingen en mappen", imgUpload:"Afbeeldingen uploaden",
|
imgFolders:"Afbeeldingen en mappen", imgUpload:"Afbeeldingen uploaden",
|
||||||
imgChooseFiles:"Kies één of meer afbeeldingen…", imgFolder:"Map", imgThemes:"Thema’s",
|
imgChooseFiles:"Kies één of meer afbeeldingen…", imgFolder:"Map", imgThemes:"Thema’s",
|
||||||
imgNewTheme:"Nieuw thema…", imgAddTheme:"Thema toevoegen", imgEdit:"Afbeelding bewerken", imgName:"Naam",
|
imgNewTheme:"Nieuw thema…", imgAddTheme:"Thema toevoegen", imgEdit:"Afbeelding bewerken", imgName:"Naam",
|
||||||
|
|
@ -324,7 +326,7 @@ const I18N = {
|
||||||
instr2_mid:"Find the <b>middle letter</b> of the word.",
|
instr2_mid:"Find the <b>middle letter</b> of the word.",
|
||||||
instr2_end:"Find the <b>ending letter</b> of the word.",
|
instr2_end:"Find the <b>ending letter</b> of the word.",
|
||||||
instr3:"Spell the word for the picture. The little studs will help you!",
|
instr3:"Spell the word for the picture. The little studs will help you!",
|
||||||
card:"Card", cardGeneral:"General", word:"Word", welldone:"Well done!", next:"Next word ▸",
|
card:"Card", cardGeneral:"General", cardStart:"Starter words", wordThemes:"Word themes", word:"Word", welldone:"Well done!", next:"Next word ▸",
|
||||||
cardDone:"Card complete! 🎉", pickCard:"Pick a new card or another level.",
|
cardDone:"Card complete! 🎉", pickCard:"Pick a new card or another level.",
|
||||||
myWords:"My words", manage:"Manage themes and words", addWord:"+ Add",
|
myWords:"My words", manage:"Manage themes and words", addWord:"+ Add",
|
||||||
pickPic:"Choose a picture",
|
pickPic:"Choose a picture",
|
||||||
|
|
@ -443,7 +445,7 @@ const I18N = {
|
||||||
wordPh:"word (2–5 letters)", close:"Close",
|
wordPh:"word (2–5 letters)", close:"Close",
|
||||||
badWord:"Use 2 to 5 letters (a-z).", needPic:"Pick a picture first.",
|
badWord:"Use 2 to 5 letters (a-z).", needPic:"Pick a picture first.",
|
||||||
noWords:"No custom words yet. Add one above!",
|
noWords:"No custom words yet. Add one above!",
|
||||||
tabEmoji:"😀 Emoji", tabPicto:"🖼️ Pictos", tabUpload:"📷 Upload",
|
tabEmoji:"😀 Emoji", tabPicto:"🖼️ Pictos", tabUpload:"📷 Upload", tabLibrary:"🗂 Library",
|
||||||
pictoSearch:"Search", pictoPh:"search word…", pictoNone:"Nothing found. Try another word.",
|
pictoSearch:"Search", pictoPh:"search word…", pictoNone:"Nothing found. Try another word.",
|
||||||
pictoLoad:"Searching…", pictoErr:"Could not load pictos (internet required).",
|
pictoLoad:"Searching…", pictoErr:"Could not load pictos (internet required).",
|
||||||
pictoCredit:"Pictos: <a href='https://arasaac.org' target='_blank'>ARASAAC</a> (CC BY-NC-SA, Gobierno de Aragón). Also nice: <a href='https://www.sclera.be' target='_blank'>sclera.be</a> (free download, then add via Upload).",
|
pictoCredit:"Pictos: <a href='https://arasaac.org' target='_blank'>ARASAAC</a> (CC BY-NC-SA, Gobierno de Aragón). Also nice: <a href='https://www.sclera.be' target='_blank'>sclera.be</a> (free download, then add via Upload).",
|
||||||
|
|
@ -451,6 +453,8 @@ const I18N = {
|
||||||
upHint:"Tip: square picture, white or transparent background, one clear object, no text in the picture.",
|
upHint:"Tip: square picture, white or transparent background, one clear object, no text in the picture.",
|
||||||
save:"Save", cancel:"Cancel",
|
save:"Save", cancel:"Cancel",
|
||||||
imgCatalog:"Image catalogue", imgSearch:"Search by name, folder or theme…",
|
imgCatalog:"Image catalogue", imgSearch:"Search by name, folder or theme…",
|
||||||
|
imgOpenLibrary:"Open image library", imgLanguageHint:"All starter pictures, custom word cards and uploads are collected here.",
|
||||||
|
imgWordThemes:"Language themes",
|
||||||
imgFolders:"Images and folders", imgUpload:"Upload images",
|
imgFolders:"Images and folders", imgUpload:"Upload images",
|
||||||
imgChooseFiles:"Choose one or more images…", imgFolder:"Folder", imgThemes:"Themes",
|
imgChooseFiles:"Choose one or more images…", imgFolder:"Folder", imgThemes:"Themes",
|
||||||
imgNewTheme:"New theme…", imgAddTheme:"Add theme", imgEdit:"Edit image", imgName:"Name",
|
imgNewTheme:"New theme…", imgAddTheme:"Add theme", imgEdit:"Edit image", imgName:"Name",
|
||||||
|
|
|
||||||
|
|
@ -24,23 +24,228 @@ function dotGrid(letter){
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* built-in general words: ONE card per language (extendable via the editor) */
|
/* De taalplaatjes hebben één centrale bron. Daardoor gebruiken de spelletjes,
|
||||||
const WORDS = {
|
de themakiezer en de afbeeldingenbibliotheek altijd dezelfde woorden en
|
||||||
nl:[
|
verdwijnen oude plaatjes niet wanneer de woordenschat wordt uitgebreid. */
|
||||||
["aap","🐵"],["vis","🐟"],["zon","☀️"],["kat","🐈"],["bal","⚽"],["ei","🥚"],
|
const LANGUAGE_THEME_DEFS = [
|
||||||
["boom","🌳"],["huis","🏠"],["maan","🌙"],["boot","⛵"],["kip","🐔"],["muis","🐭"],
|
{id:"animals", icon:"🐾", nl:"Dieren", en:"Animals", colors:["#eaf7df","#cdeeb5"]},
|
||||||
["boek","📕"],["ster","⭐"],["roos","🌹"],["vuur","🔥"],["fiets","🚲"],["taart","🎂"],
|
{id:"food", icon:"🍎", nl:"Eten & drinken", en:"Food & drink", colors:["#fff0dc","#ffd9aa"]},
|
||||||
["jas","🧥"],["bril","👓"],["klok","⏰"],["peer","🍐"],["kaas","🧀"],["eend","🦆"],
|
{id:"nature", icon:"🌿", nl:"Natuur & weer", en:"Nature & weather", colors:["#e4f7ee","#bfead4"]},
|
||||||
["hand","✋"],["voet","🦶"],["neus","👃"],["oog","👁️"],["oor","👂"],["mond","👄"]
|
{id:"travel", icon:"🚲", nl:"Onderweg", en:"Getting around", colors:["#e6f3ff","#beddf8"]},
|
||||||
],
|
{id:"home", icon:"🎒", nl:"Thuis & school", en:"Home & school", colors:["#f1ecff","#d9ccff"]},
|
||||||
en:[
|
{id:"body", icon:"👕", nl:"Lichaam & kleding", en:"Body & clothes", colors:["#ffeaf1","#ffcbdc"]},
|
||||||
["cat","🐈"],["dog","🐕"],["sun","☀️"],["egg","🥚"],["ball","⚽"],["fish","🐟"],
|
{id:"actions", icon:"🏃", nl:"Doen", en:"Actions", colors:["#fff8d9","#f6e8a4"]},
|
||||||
["tree","🌳"],["moon","🌙"],["boat","⛵"],["star","⭐"],["book","📕"],["cake","🎂"],
|
{id:"feelings", icon:"🙂", nl:"Gevoelens", en:"Feelings", colors:["#e9f5ff","#c8e4fa"]}
|
||||||
["house","🏠"],["mouse","🐭"],["apple","🍎"],["train","🚂"],["fire","🔥"],["rose","🌹"],
|
];
|
||||||
["bear","🐻"],["duck","🦆"],["frog","🐸"],["bee","🐝"],["cow","🐄"],["pig","🐷"],
|
const LANGUAGE_CONCEPTS = [
|
||||||
["hand","✋"],["foot","🦶"],["nose","👃"],["eye","👁️"],["ear","👂"],["milk","🥛"]
|
["monkey","animals","aap","monkey","🐵"],
|
||||||
]
|
["cat","animals","kat","cat","🐈"],
|
||||||
|
["dog","animals","hond","dog","🐕"],
|
||||||
|
["fish","animals","vis","fish","🐟"],
|
||||||
|
["hen","animals","kip","hen","🐔"],
|
||||||
|
["mouse","animals","muis","mouse","🐭"],
|
||||||
|
["bear","animals","beer","bear","🐻"],
|
||||||
|
["duck","animals","eend","duck","🦆"],
|
||||||
|
["frog","animals","kikker","frog","🐸"],
|
||||||
|
["bee","animals","bij","bee","🐝"],
|
||||||
|
["cow","animals","koe","cow","🐄"],
|
||||||
|
["pig","animals","varken","pig","🐷"],
|
||||||
|
["owl","animals","uil","owl","🦉"],
|
||||||
|
["goat","animals","geit","goat","🐐"],
|
||||||
|
["hare","animals","haas","hare","🐇"],
|
||||||
|
["horse","animals","paard","horse","🐴"],
|
||||||
|
["lion","animals","leeuw","lion","🦁"],
|
||||||
|
["ant","animals","mier","ant","🐜"],
|
||||||
|
["egg","food","ei","egg","🥚"],
|
||||||
|
["apple","food","appel","apple","🍎"],
|
||||||
|
["pear","food","peer","pear","🍐"],
|
||||||
|
["cheese","food","kaas","cheese","🧀"],
|
||||||
|
["milk","food","melk","milk","🥛"],
|
||||||
|
["cake","food","taart","cake","🎂"],
|
||||||
|
["bread","food","brood","bread","🍞"],
|
||||||
|
["soup","food","soep","soup","🥣"],
|
||||||
|
["rice","food","rijst","rice","🍚"],
|
||||||
|
["corn","food","mais","corn","🌽"],
|
||||||
|
["grape","food","druif","grape","🍇"],
|
||||||
|
["strawberry","food","aardbei","strawberry","🍓"],
|
||||||
|
["candy","food","snoep","candy","🍬"],
|
||||||
|
["lemon","food","citroen","lemon","🍋"],
|
||||||
|
["pizza","food","pizza","pizza","🍕"],
|
||||||
|
["juice","food","sap","juice","🧃"],
|
||||||
|
["cookie","food","koek","cookie","🍪"],
|
||||||
|
["carrot","food","wortel","carrot","🥕"],
|
||||||
|
["sun","nature","zon","sun","☀️"],
|
||||||
|
["moon","nature","maan","moon","🌙"],
|
||||||
|
["star","nature","ster","star","⭐"],
|
||||||
|
["tree","nature","boom","tree","🌳"],
|
||||||
|
["rose","nature","roos","rose","🌹"],
|
||||||
|
["fire","nature","vuur","fire","🔥"],
|
||||||
|
["flower","nature","bloem","flower","🌷"],
|
||||||
|
["leaf","nature","blad","leaf","🍃"],
|
||||||
|
["cloud","nature","wolk","cloud","☁️"],
|
||||||
|
["rain","nature","regen","rain","🌧️"],
|
||||||
|
["snow","nature","sneeuw","snow","❄️"],
|
||||||
|
["water","nature","water","water","💧"],
|
||||||
|
["stone","nature","steen","stone","🪨"],
|
||||||
|
["sand","nature","zand","sand","🏖️"],
|
||||||
|
["sea","nature","zee","sea","🌊"],
|
||||||
|
["forest","nature","bos","forest","🌲"],
|
||||||
|
["grass","nature","gras","grass","🌱"],
|
||||||
|
["wind","nature","wind","wind","💨"],
|
||||||
|
["rainbow","nature","regenboog","rainbow","🌈"],
|
||||||
|
["car","travel","auto","car","🚗"],
|
||||||
|
["bike","travel","fiets","bike","🚲"],
|
||||||
|
["boat","travel","boot","boat","⛵"],
|
||||||
|
["train","travel","trein","train","🚂"],
|
||||||
|
["bus","travel","bus","bus","🚌"],
|
||||||
|
["taxi","travel","taxi","taxi","🚕"],
|
||||||
|
["truck","travel","vrachtauto","truck","🚚"],
|
||||||
|
["jet","travel","vliegtuig","plane","✈️"],
|
||||||
|
["tram","travel","tram","tram","🚋"],
|
||||||
|
["ship","travel","schip","ship","🚢"],
|
||||||
|
["rocket","travel","raket","rocket","🚀"],
|
||||||
|
["road","travel","weg","road","🛣️"],
|
||||||
|
["wheel","travel","wiel","wheel","🛞"],
|
||||||
|
["sail","travel","zeil","sail","⛵"],
|
||||||
|
["scooter","travel","step","scooter","🛴"],
|
||||||
|
["tractor","travel","tractor","tractor","🚜"],
|
||||||
|
["helicopter","travel","helikopter","helicopter","🚁"],
|
||||||
|
["house","home","huis","house","🏠"],
|
||||||
|
["book","home","boek","book","📕"],
|
||||||
|
["pen","home","pen","pen","🖊️"],
|
||||||
|
["pencil","home","potlood","pencil","✏️"],
|
||||||
|
["bag","home","tas","bag","🎒"],
|
||||||
|
["coat","home","jas","coat","🧥"],
|
||||||
|
["chair","home","stoel","chair","🪑"],
|
||||||
|
["bed","home","bed","bed","🛏️"],
|
||||||
|
["door","home","deur","door","🚪"],
|
||||||
|
["clock","home","klok","clock","⏰"],
|
||||||
|
["glasses","home","bril","glasses","👓"],
|
||||||
|
["ball","home","bal","ball","⚽"],
|
||||||
|
["doll","home","pop","doll","🪆"],
|
||||||
|
["block","home","blok","block","🧱"],
|
||||||
|
["key","home","sleutel","key","🔑"],
|
||||||
|
["lamp","home","lamp","lamp","💡"],
|
||||||
|
["cup","home","beker","cup","🥤"],
|
||||||
|
["brush","home","borstel","brush","🪥"],
|
||||||
|
["drum","home","trommel","drum","🥁"],
|
||||||
|
["gift","home","cadeau","gift","🎁"],
|
||||||
|
["paint","home","verf","paint","🎨"],
|
||||||
|
["scissors","home","schaar","scissors","✂️"],
|
||||||
|
["desk","home","bureau","desk","🗄️"],
|
||||||
|
["school","home","school","school","🏫"],
|
||||||
|
["hand","body","hand","hand","✋"],
|
||||||
|
["foot","body","voet","foot","🦶"],
|
||||||
|
["nose","body","neus","nose","👃"],
|
||||||
|
["eye","body","oog","eye","👁️"],
|
||||||
|
["ear","body","oor","ear","👂"],
|
||||||
|
["mouth","body","mond","mouth","👄"],
|
||||||
|
["hair","body","haar","hair","🧑🦱"],
|
||||||
|
["head","body","hoofd","head","🙂"],
|
||||||
|
["arm","body","arm","arm","💪"],
|
||||||
|
["leg","body","been","leg","🦵"],
|
||||||
|
["cap","body","pet","cap","🧢"],
|
||||||
|
["sock","body","sok","sock","🧦"],
|
||||||
|
["shoe","body","schoen","shoe","👟"],
|
||||||
|
["dress","body","jurk","dress","👗"],
|
||||||
|
["shirt","body","shirt","shirt","👕"],
|
||||||
|
["glove","body","want","glove","🧤"],
|
||||||
|
["pants","body","broek","trousers","👖"],
|
||||||
|
["skirt","body","rok","skirt","👗"],
|
||||||
|
["tooth","body","tand","tooth","🦷"],
|
||||||
|
["read","actions","lezen","read","📖"],
|
||||||
|
["eat","actions","eten","eat","🍽️"],
|
||||||
|
["drink","actions","drinken","drink","🥤"],
|
||||||
|
["sleep","actions","slapen","sleep","😴"],
|
||||||
|
["walk","actions","lopen","walk","🚶"],
|
||||||
|
["swim","actions","zwemmen","swim","🏊"],
|
||||||
|
["jump","actions","springen","jump","🤸"],
|
||||||
|
["laugh","actions","lachen","laugh","😄"],
|
||||||
|
["cry","actions","huilen","cry","😢"],
|
||||||
|
["sing","actions","zingen","sing","🎤"],
|
||||||
|
["dance","actions","dansen","dance","💃"],
|
||||||
|
["draw","actions","tekenen","draw","🖍️"],
|
||||||
|
["write","actions","schrijven","write","✍️"],
|
||||||
|
["play","actions","spelen","play","🧸"],
|
||||||
|
["sit","actions","zitten","sit","🧎"],
|
||||||
|
["run","actions","rennen","run","🏃"],
|
||||||
|
["climb","actions","klimmen","climb","🧗"],
|
||||||
|
["wave","actions","zwaaien","wave","👋"],
|
||||||
|
["look","actions","kijken","look","👀"],
|
||||||
|
["think","actions","denken","think","🤔"],
|
||||||
|
["happy","feelings","blij","happy","😀"],
|
||||||
|
["sad","feelings","verdrietig","sad","😢"],
|
||||||
|
["angry","feelings","boos","angry","😠"],
|
||||||
|
["scared","feelings","bang","scared","😨"],
|
||||||
|
["tired","feelings","moe","tired","🥱"],
|
||||||
|
["sick","feelings","ziek","sick","🤒"],
|
||||||
|
["proud","feelings","trots","proud","😌"],
|
||||||
|
["calm","feelings","rustig","calm","🙂"],
|
||||||
|
["surprised","feelings","verrast","surprised","😮"],
|
||||||
|
["excited","feelings","enthousiast","excited","🤩"]
|
||||||
|
].map(([id,theme,nl,en,pic])=>({id,theme,nl,en,pic}));
|
||||||
|
|
||||||
|
/* De vertrouwde startkaart blijft exact dezelfde 30 woorden houden. */
|
||||||
|
const START_WORD_KEYS = {
|
||||||
|
nl:["monkey","fish","sun","cat","ball","egg","tree","house","moon","boat","hen","mouse","book","star","rose","fire","bike","cake","coat","glasses","clock","pear","cheese","duck","hand","foot","nose","eye","ear","mouth"],
|
||||||
|
en:["cat","dog","sun","egg","ball","fish","tree","moon","boat","star","book","cake","house","mouse","apple","train","fire","rose","bear","duck","frog","bee","cow","pig","hand","foot","nose","eye","ear","milk"]
|
||||||
};
|
};
|
||||||
|
const WORDS = Object.fromEntries(["nl","en"].map(lang=>[
|
||||||
|
lang, START_WORD_KEYS[lang].map(id=>{
|
||||||
|
const concept=LANGUAGE_CONCEPTS.find(item=>item.id===id);
|
||||||
|
return [concept[lang],concept.pic];
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
|
||||||
|
function languageThemeList(lang=LANG){
|
||||||
|
return LANGUAGE_THEME_DEFS.map(theme=>({
|
||||||
|
...theme,
|
||||||
|
name:theme[lang],
|
||||||
|
words:LANGUAGE_CONCEPTS.filter(item=>item.theme===theme.id).map(item=>[item[lang],item.pic])
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function languageEmojiDataUrl(emoji,colors=["#f4f8ff","#dce9fa"]){
|
||||||
|
const svg=`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320" viewBox="0 0 320 320"><defs><linearGradient id="g" x2="1" y2="1"><stop stop-color="${colors[0]}"/><stop offset="1" stop-color="${colors[1]}"/></linearGradient></defs><rect width="320" height="320" rx="48" fill="url(#g)"/><circle cx="160" cy="160" r="112" fill="#fff" opacity=".92"/><text x="160" y="177" text-anchor="middle" dominant-baseline="middle" font-family="Apple Color Emoji,Segoe UI Emoji,Noto Color Emoji,sans-serif" font-size="148">${emoji}</text></svg>`;
|
||||||
|
return "data:image/svg+xml;charset=utf-8,"+encodeURIComponent(svg);
|
||||||
|
}
|
||||||
|
function languagePictureCatalog(lang=LANG){
|
||||||
|
const builtinThemes=languageThemeList(lang).map((theme,index)=>({
|
||||||
|
id:-200-index,nameNl:theme.nl,nameEn:theme.en,scope:"global",filterOnly:true,
|
||||||
|
languageBuiltin:true,wordPicture:true,icon:theme.icon
|
||||||
|
}));
|
||||||
|
const themeIds=new Map(LANGUAGE_THEME_DEFS.map((theme,index)=>[theme.id,-200-index]));
|
||||||
|
const images=LANGUAGE_CONCEPTS.map((item,index)=>{
|
||||||
|
const theme=LANGUAGE_THEME_DEFS.find(def=>def.id===item.theme);
|
||||||
|
return {
|
||||||
|
id:-1000-index,name:item[lang],folder:`Taal/Woordenschat/${theme[lang]}`,
|
||||||
|
scope:"global",themes:[themeIds.get(item.theme)],tags:[item.nl,item.en,theme.nl,theme.en],
|
||||||
|
src:languageEmojiDataUrl(item.pic,theme.colors),value:item.pic,canManage:false,canDelete:false,
|
||||||
|
wordPicture:true,builtin:true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const ownThemes=[];
|
||||||
|
const customImages=[];
|
||||||
|
let customId=-5000;
|
||||||
|
const addOwn=(words,name,folder,themeId)=>{
|
||||||
|
(words||[]).forEach(([word,pic])=>{
|
||||||
|
if(!word||!pic)return;
|
||||||
|
customImages.push({
|
||||||
|
id:customId--,name:word,folder,scope:"personal",themes:[themeId],tags:[word,name],
|
||||||
|
src:isImg(pic)?pic:languageEmojiDataUrl(pic),value:pic,canManage:false,canDelete:false,
|
||||||
|
wordPicture:true,builtin:false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const generalThemeId=-400;
|
||||||
|
ownThemes.push({id:generalThemeId,nameNl:"Mijn woordkaarten",nameEn:"My word cards",scope:"personal",filterOnly:true,wordPicture:true,icon:"⭐"});
|
||||||
|
addOwn(GENERAL_EXTRA[lang],lang==="nl"?"Mijn woordkaarten":"My word cards","Taal/Mijn woordkaarten",generalThemeId);
|
||||||
|
THEMES[lang].forEach((theme,index)=>{
|
||||||
|
const id=-401-index;
|
||||||
|
ownThemes.push({id,nameNl:theme.name,nameEn:theme.name,scope:"personal",filterOnly:true,wordPicture:true,icon:"📁"});
|
||||||
|
addOwn(theme.words,theme.name,["Taal/Mijn thema’s",theme.folder,theme.name].filter(Boolean).join("/"),id);
|
||||||
|
});
|
||||||
|
return {themes:builtinThemes.concat(ownThemes),images:images.concat(customImages)};
|
||||||
|
}
|
||||||
/* teacher's own additions to the general card (persisted per user) */
|
/* teacher's own additions to the general card (persisted per user) */
|
||||||
let GENERAL_EXTRA = {nl:[], en:[]};
|
let GENERAL_EXTRA = {nl:[], en:[]};
|
||||||
function generalList(){ return WORDS[LANG].concat(GENERAL_EXTRA[LANG]); }
|
function generalList(){ return WORDS[LANG].concat(GENERAL_EXTRA[LANG]); }
|
||||||
|
|
@ -66,7 +271,7 @@ function themesFromData(data){
|
||||||
});
|
});
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
const isImg = p => typeof p==="string" && (p.startsWith("data:") || p.startsWith("http"));
|
const isImg = p => typeof p==="string" && /^(?:data:|https?:|blob:|\/)/.test(p);
|
||||||
function setPic(el, pic){
|
function setPic(el, pic){
|
||||||
if(isImg(pic)){ el.innerHTML = ""; const im = document.createElement("img"); im.src = pic; im.alt=""; el.appendChild(im); }
|
if(isImg(pic)){ el.innerHTML = ""; const im = document.createElement("img"); im.src = pic; im.alt=""; el.appendChild(im); }
|
||||||
else el.textContent = pic;
|
else el.textContent = pic;
|
||||||
|
|
@ -90,7 +295,9 @@ function anchorSources(){
|
||||||
}
|
}
|
||||||
function normWordSource(card){
|
function normWordSource(card){
|
||||||
if(typeof card==="string"){
|
if(typeof card==="string"){
|
||||||
if(card.startsWith("t")){
|
if(card.startsWith("b:")){
|
||||||
|
if(languageThemeList().some(theme=>theme.id===card.slice(2))) return card;
|
||||||
|
}else if(card.startsWith("t")){
|
||||||
const th = THEMES[LANG][+card.slice(1)];
|
const th = THEMES[LANG][+card.slice(1)];
|
||||||
if(th && th.words.length) return card;
|
if(th && th.words.length) return card;
|
||||||
}else if(card.startsWith("a")){
|
}else if(card.startsWith("a")){
|
||||||
|
|
@ -101,6 +308,7 @@ function normWordSource(card){
|
||||||
}
|
}
|
||||||
function wordSourceList(card){
|
function wordSourceList(card){
|
||||||
card = normWordSource(card);
|
card = normWordSource(card);
|
||||||
|
if(card.startsWith("b:")) return languageThemeList().find(theme=>theme.id===card.slice(2)).words;
|
||||||
if(card.startsWith("t")) return THEMES[LANG][+card.slice(1)].words;
|
if(card.startsWith("t")) return THEMES[LANG][+card.slice(1)].words;
|
||||||
if(card.startsWith("a")) return anchorSources()[+card.slice(1)].words;
|
if(card.startsWith("a")) return anchorSources()[+card.slice(1)].words;
|
||||||
return generalList();
|
return generalList();
|
||||||
|
|
@ -108,8 +316,17 @@ function wordSourceList(card){
|
||||||
function fillWordSourceSel(sel, card){
|
function fillWordSourceSel(sel, card){
|
||||||
sel.innerHTML = "";
|
sel.innerHTML = "";
|
||||||
const og = document.createElement("option");
|
const og = document.createElement("option");
|
||||||
og.value = "g"; og.textContent = T("cardGeneral");
|
og.value = "g"; og.textContent = "⭐ "+T("cardStart");
|
||||||
sel.appendChild(og);
|
sel.appendChild(og);
|
||||||
|
const builtinGroup = document.createElement("optgroup");
|
||||||
|
builtinGroup.label = "📚 "+T("wordThemes");
|
||||||
|
languageThemeList().forEach(theme=>{
|
||||||
|
const option=document.createElement("option");
|
||||||
|
option.value="b:"+theme.id;
|
||||||
|
option.textContent=`${theme.icon} ${theme.name} (${theme.words.length})`;
|
||||||
|
builtinGroup.appendChild(option);
|
||||||
|
});
|
||||||
|
sel.appendChild(builtinGroup);
|
||||||
/* thema's zonder map bovenaan, daarna per map gegroepeerd (optgroup) -
|
/* thema's zonder map bovenaan, daarna per map gegroepeerd (optgroup) -
|
||||||
zelfde indeling als de theme-dropdown in de woorden-editor */
|
zelfde indeling als de theme-dropdown in de woorden-editor */
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
|
|
@ -158,4 +375,3 @@ const EMOJI_CATS = [
|
||||||
splitEmoji("⚽🏀🎈🎁🧸📕✏️🖍️✂️🎨🥁🎸👓⌚⏰💡🔑🧤🧢👟👗🧥🛏️🪑🚪🏠"),
|
splitEmoji("⚽🏀🎈🎁🧸📕✏️🖍️✂️🎨🥁🎸👓⌚⏰💡🔑🧤🧢👟👗🧥🛏️🪑🚪🏠"),
|
||||||
splitEmoji("✋👂👃👁️👄🦶😀😢😴🤧👶👦👧👵👴")
|
splitEmoji("✋👂👃👁️👄🦶😀😢😴🤧👶👦👧👵👴")
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,18 @@ async function uploadImageToCatalog(file, options={}){
|
||||||
|
|
||||||
async function loadImageCatalog(){
|
async function loadImageCatalog(){
|
||||||
IMAGE_CATALOG=await api("/images");
|
IMAGE_CATALOG=await api("/images");
|
||||||
|
refreshWordPictureCatalog();
|
||||||
return IMAGE_CATALOG;
|
return IMAGE_CATALOG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshWordPictureCatalog(){
|
||||||
|
const wordCatalog=languagePictureCatalog(LANG);
|
||||||
|
IMAGE_CATALOG.images=(IMAGE_CATALOG.images||[]).filter(image=>!image.wordPicture);
|
||||||
|
IMAGE_CATALOG.themes=(IMAGE_CATALOG.themes||[]).filter(theme=>!theme.wordPicture);
|
||||||
|
IMAGE_CATALOG.images=wordCatalog.images.concat(IMAGE_CATALOG.images);
|
||||||
|
IMAGE_CATALOG.themes=wordCatalog.themes.concat(IMAGE_CATALOG.themes);
|
||||||
|
}
|
||||||
|
|
||||||
function ensureImageCatalog(){
|
function ensureImageCatalog(){
|
||||||
if(imageCatalogWrap)return imageCatalogWrap;
|
if(imageCatalogWrap)return imageCatalogWrap;
|
||||||
const wrap=document.createElement("div");
|
const wrap=document.createElement("div");
|
||||||
|
|
@ -51,6 +60,7 @@ function ensureImageCatalog(){
|
||||||
<select class="ic-folder-filter"></select>
|
<select class="ic-folder-filter"></select>
|
||||||
<button class="tbtn ghost ic-folders" type="button"></button>
|
<button class="tbtn ghost ic-folders" type="button"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ic-word-themes"></div>
|
||||||
<div class="ic-quota"></div>
|
<div class="ic-quota"></div>
|
||||||
<details class="ic-add">
|
<details class="ic-add">
|
||||||
<summary></summary>
|
<summary></summary>
|
||||||
|
|
@ -78,7 +88,7 @@ function ensureImageCatalog(){
|
||||||
q(".ic-close").onclick=closeImageCatalog;
|
q(".ic-close").onclick=closeImageCatalog;
|
||||||
wrap.onclick=e=>{if(e.target===wrap)closeImageCatalog()};
|
wrap.onclick=e=>{if(e.target===wrap)closeImageCatalog()};
|
||||||
q(".ic-search").oninput=renderImageCatalogGrid;
|
q(".ic-search").oninput=renderImageCatalogGrid;
|
||||||
q(".ic-theme-filter").onchange=renderImageCatalogGrid;
|
q(".ic-theme-filter").onchange=()=>{renderImageCatalogGrid();renderLanguageThemeChips()};
|
||||||
q(".ic-scope-filter").onchange=renderImageCatalogGrid;
|
q(".ic-scope-filter").onchange=renderImageCatalogGrid;
|
||||||
q(".ic-folder-filter").onchange=renderImageCatalogGrid;
|
q(".ic-folder-filter").onchange=renderImageCatalogGrid;
|
||||||
q(".ic-folders").onclick=openImageFolderExplorer;
|
q(".ic-folders").onclick=openImageFolderExplorer;
|
||||||
|
|
@ -125,14 +135,33 @@ function fillImageSelect(select,items,value,label){
|
||||||
function fillThemeSelect(select,selected=[],includeAll=false,asset=null){
|
function fillThemeSelect(select,selected=[],includeAll=false,asset=null){
|
||||||
const current=new Set((selected||[]).map(Number));select.innerHTML="";
|
const current=new Set((selected||[]).map(Number));select.innerHTML="";
|
||||||
if(includeAll)select.append(new Option(T("imgAllThemes"),""));
|
if(includeAll)select.append(new Option(T("imgAllThemes"),""));
|
||||||
IMAGE_CATALOG.themes.filter(theme=>!asset||theme.scope==="global"
|
IMAGE_CATALOG.themes.filter(theme=>(includeAll||!theme.filterOnly)&&(!asset||theme.scope==="global"
|
||||||
||(asset.scope==="school"&&theme.scope==="school")
|
||(asset.scope==="school"&&theme.scope==="school")
|
||||||
||(asset.scope==="personal"&&theme.scope==="personal"))
|
||(asset.scope==="personal"&&theme.scope==="personal")))
|
||||||
.forEach(theme=>{
|
.forEach(theme=>{
|
||||||
const option=new Option(imageThemeName(theme),theme.id);
|
const option=new Option(imageThemeName(theme),theme.id);
|
||||||
option.selected=current.has(theme.id);select.append(option);
|
option.selected=current.has(theme.id);select.append(option);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function renderLanguageThemeChips(){
|
||||||
|
if(!imageCatalogWrap)return;
|
||||||
|
const host=imageCatalogWrap.querySelector(".ic-word-themes");
|
||||||
|
const select=imageCatalogWrap.querySelector(".ic-theme-filter");
|
||||||
|
const themes=IMAGE_CATALOG.themes.filter(theme=>theme.languageBuiltin);
|
||||||
|
host.innerHTML="";host.hidden=!themes.length;
|
||||||
|
if(!themes.length)return;
|
||||||
|
const label=document.createElement("strong");label.textContent=T("imgWordThemes");host.append(label);
|
||||||
|
themes.forEach(theme=>{
|
||||||
|
const button=document.createElement("button");button.type="button";
|
||||||
|
button.textContent=(theme.icon||"🖼️")+" "+imageThemeName(theme);
|
||||||
|
button.classList.toggle("active",select.value===String(theme.id));
|
||||||
|
button.onclick=()=>{
|
||||||
|
select.value=select.value===String(theme.id)?"":String(theme.id);
|
||||||
|
renderImageCatalogGrid();renderLanguageThemeChips();
|
||||||
|
};
|
||||||
|
host.append(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
function applyImageCatalogLabels(){
|
function applyImageCatalogLabels(){
|
||||||
const q=s=>imageCatalogWrap.querySelector(s);
|
const q=s=>imageCatalogWrap.querySelector(s);
|
||||||
q(".ic-title").textContent=T("imgCatalog");
|
q(".ic-title").textContent=T("imgCatalog");
|
||||||
|
|
@ -155,6 +184,7 @@ function applyImageCatalogLabels(){
|
||||||
}
|
}
|
||||||
function renderImageCatalog(){
|
function renderImageCatalog(){
|
||||||
const q=s=>imageCatalogWrap.querySelector(s);
|
const q=s=>imageCatalogWrap.querySelector(s);
|
||||||
|
refreshWordPictureCatalog();
|
||||||
applyImageCatalogLabels();
|
applyImageCatalogLabels();
|
||||||
const themeValue=q(".ic-theme-filter").value,scopeValue=q(".ic-scope-filter").value,folderValue=q(".ic-folder-filter").value;
|
const themeValue=q(".ic-theme-filter").value,scopeValue=q(".ic-scope-filter").value,folderValue=q(".ic-folder-filter").value;
|
||||||
fillThemeSelect(q(".ic-theme-filter"),[],true);
|
fillThemeSelect(q(".ic-theme-filter"),[],true);
|
||||||
|
|
@ -170,6 +200,7 @@ function renderImageCatalog(){
|
||||||
const ownFolders=catalogOwnFolders().map(path=>({value:path,label:path||T("imgFolderRoot")}));
|
const ownFolders=catalogOwnFolders().map(path=>({value:path,label:path||T("imgFolderRoot")}));
|
||||||
fillImageSelect(q(".ic-upload-folder"),ownFolders,q(".ic-upload-folder").value);
|
fillImageSelect(q(".ic-upload-folder"),ownFolders,q(".ic-upload-folder").value);
|
||||||
fillThemeSelect(q(".ic-upload-themes"),[...q(".ic-upload-themes").selectedOptions].map(o=>+o.value),false,{scope:IMAGE_CATALOG.uploadScope});
|
fillThemeSelect(q(".ic-upload-themes"),[...q(".ic-upload-themes").selectedOptions].map(o=>+o.value),false,{scope:IMAGE_CATALOG.uploadScope});
|
||||||
|
renderLanguageThemeChips();
|
||||||
const quota=IMAGE_CATALOG.quota||{},limit=quota.limitBytes;
|
const quota=IMAGE_CATALOG.quota||{},limit=quota.limitBytes;
|
||||||
let text=T("imgQuotaUsed").replace("{used}",imageBytes(quota.usedBytes));
|
let text=T("imgQuotaUsed").replace("{used}",imageBytes(quota.usedBytes));
|
||||||
text+=" · "+(limit==null?T("imgUnlimited"):T("imgQuotaOf").replace("{limit}",imageBytes(limit)));
|
text+=" · "+(limit==null?T("imgUnlimited"):T("imgQuotaOf").replace("{limit}",imageBytes(limit)));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* teach - herbruikbare plaatjeskiezer (emoji / ARASAAC-zoeken / eigen upload).
|
/* teach - herbruikbare plaatjeskiezer (bibliotheek / emoji / ARASAAC / upload).
|
||||||
Bouwt voort op dezelfde bouwstenen als de woordkaart-editor in letters.js
|
Bouwt voort op dezelfde bouwstenen als de woordkaart-editor in letters.js
|
||||||
(EMOJI_CATS, arasaacSearch/arasaacUrl, setPic, dezelfde CSS-klassen) i.p.v.
|
(EMOJI_CATS, arasaacSearch/arasaacUrl, setPic, dezelfde CSS-klassen) i.p.v.
|
||||||
een tweede plaatjessysteem te maken. Gebruikt door widgets die snel een los
|
een tweede plaatjessysteem te maken. Gebruikt door widgets die snel een los
|
||||||
|
|
@ -30,6 +30,7 @@ function ensurePicPicker(){
|
||||||
<button type="button" data-t="emoji"></button>
|
<button type="button" data-t="emoji"></button>
|
||||||
<button type="button" data-t="picto"></button>
|
<button type="button" data-t="picto"></button>
|
||||||
<button type="button" data-t="upload"></button>
|
<button type="button" data-t="upload"></button>
|
||||||
|
<button type="button" data-t="library"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ed-pane">
|
<div class="ed-pane">
|
||||||
<div class="ed-p-emoji">
|
<div class="ed-p-emoji">
|
||||||
|
|
@ -49,6 +50,10 @@ function ensurePicPicker(){
|
||||||
<label class="ed-upload"><span class="ed-uplbl"></span><input type="file" accept="image/*"></label>
|
<label class="ed-upload"><span class="ed-uplbl"></span><input type="file" accept="image/*"></label>
|
||||||
<div class="ed-uphint"></div>
|
<div class="ed-uphint"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ed-p-library" style="display:none;">
|
||||||
|
<button class="tbtn ed-open-library" type="button"></button>
|
||||||
|
<p class="ed-library-hint"></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(wrap);
|
document.body.appendChild(wrap);
|
||||||
|
|
@ -66,6 +71,8 @@ function ensurePicPicker(){
|
||||||
q(".ed-credit").innerHTML = T("pictoCredit");
|
q(".ed-credit").innerHTML = T("pictoCredit");
|
||||||
q(".ed-pgo").textContent = T("pictoSearch");
|
q(".ed-pgo").textContent = T("pictoSearch");
|
||||||
q(".ed-psearch").placeholder = T("pictoPh");
|
q(".ed-psearch").placeholder = T("pictoPh");
|
||||||
|
q(".ed-open-library").textContent = "🗂 "+T("imgOpenLibrary");
|
||||||
|
q(".ed-library-hint").textContent = T("imgLanguageHint");
|
||||||
q(".pp-close").title = T("close");
|
q(".pp-close").title = T("close");
|
||||||
refreshTabs();
|
refreshTabs();
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +84,7 @@ function ensurePicPicker(){
|
||||||
q(".ed-p-emoji").style.display = tab==="emoji" ? "block" : "none";
|
q(".ed-p-emoji").style.display = tab==="emoji" ? "block" : "none";
|
||||||
q(".ed-p-picto").style.display = tab==="picto" ? "block" : "none";
|
q(".ed-p-picto").style.display = tab==="picto" ? "block" : "none";
|
||||||
q(".ed-p-upload").style.display = tab==="upload" ? "block" : "none";
|
q(".ed-p-upload").style.display = tab==="upload" ? "block" : "none";
|
||||||
|
q(".ed-p-library").style.display = tab==="library" ? "block" : "none";
|
||||||
}
|
}
|
||||||
wrap.querySelectorAll(".ed-tabs button").forEach(b=>{
|
wrap.querySelectorAll(".ed-tabs button").forEach(b=>{
|
||||||
b.addEventListener("click", ()=>{ tab = b.dataset.t; refreshTabs(); });
|
b.addEventListener("click", ()=>{ tab = b.dataset.t; refreshTabs(); });
|
||||||
|
|
@ -125,9 +133,22 @@ function ensurePicPicker(){
|
||||||
}
|
}
|
||||||
q(".ed-pgo").addEventListener("click", searchPictos);
|
q(".ed-pgo").addEventListener("click", searchPictos);
|
||||||
q(".ed-psearch").addEventListener("keydown", e=>{ if(e.key==="Enter") searchPictos(); });
|
q(".ed-psearch").addEventListener("keydown", e=>{ if(e.key==="Enter") searchPictos(); });
|
||||||
|
q(".ed-open-library").addEventListener("click", ()=>{
|
||||||
|
openImageCatalog((src,image)=>choosePic(image?.value||src));
|
||||||
|
});
|
||||||
|
|
||||||
q(".ed-p-upload input").addEventListener("change", e=>{
|
q(".ed-p-upload input").addEventListener("change", async e=>{
|
||||||
const f = e.target.files[0]; if(!f) return;
|
const f = e.target.files[0]; if(!f) return;
|
||||||
|
e.target.value = "";
|
||||||
|
if(currentUser){
|
||||||
|
const hint=q(".ed-uphint"),old=hint.textContent;
|
||||||
|
hint.textContent=T("imgUploading");
|
||||||
|
try{
|
||||||
|
const image=await uploadImageToCatalog(f,{folder:"Taal/Mijn woordkaarten"});
|
||||||
|
choosePic(image.src);
|
||||||
|
}catch(err){hint.textContent=err.message;setTimeout(()=>{hint.textContent=old},3500)}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = ()=>{
|
img.onload = ()=>{
|
||||||
const c = document.createElement("canvas");
|
const c = document.createElement("canvas");
|
||||||
|
|
@ -138,7 +159,6 @@ function ensurePicPicker(){
|
||||||
URL.revokeObjectURL(img.src);
|
URL.revokeObjectURL(img.src);
|
||||||
};
|
};
|
||||||
img.src = URL.createObjectURL(f);
|
img.src = URL.createObjectURL(f);
|
||||||
e.target.value = "";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
wrap.__applyLabels = applyLabels;
|
wrap.__applyLabels = applyLabels;
|
||||||
|
|
@ -154,9 +174,9 @@ function openPicPicker(word, onSelect){
|
||||||
wrap.__applyLabels();
|
wrap.__applyLabels();
|
||||||
wrap.__renderCats();
|
wrap.__renderCats();
|
||||||
wrap.querySelector(".ed-psearch").value = word || "";
|
wrap.querySelector(".ed-psearch").value = word || "";
|
||||||
/* met een woord starten we meteen op het picto-zoekresultaat i.p.v. het emoji-tabblad,
|
/* Bij een woord staat de herbruikbare, thematisch ingedeelde bibliotheek
|
||||||
anders lijkt de vooraf ingevulde zoekopdracht nergens naartoe te leiden */
|
voorop. Zonder account blijft de bestaande online pictozoeker beschikbaar. */
|
||||||
wrap.__setTab(word ? "picto" : "emoji");
|
wrap.__setTab(word ? (currentUser ? "library" : "picto") : "emoji");
|
||||||
wrap.classList.add("open");
|
wrap.classList.add("open");
|
||||||
if(word) wrap.__searchPictos();
|
if(word && !currentUser) wrap.__searchPictos();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ function mountLetterGame(root, initState, opts){
|
||||||
<div class="ed-msg"></div>
|
<div class="ed-msg"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ed-tabs">
|
<div class="ed-tabs">
|
||||||
<button data-t="emoji"></button><button data-t="picto"></button><button data-t="upload"></button>
|
<button data-t="emoji"></button><button data-t="picto"></button><button data-t="upload"></button><button data-t="library"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ed-pane">
|
<div class="ed-pane">
|
||||||
<div class="ed-p-emoji">
|
<div class="ed-p-emoji">
|
||||||
|
|
@ -69,6 +69,10 @@ function mountLetterGame(root, initState, opts){
|
||||||
<label class="ed-upload"><span class="ed-uplbl"></span><input type="file" accept="image/*"></label>
|
<label class="ed-upload"><span class="ed-uplbl"></span><input type="file" accept="image/*"></label>
|
||||||
<div class="ed-uphint"></div>
|
<div class="ed-uphint"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ed-p-library" style="display:none;">
|
||||||
|
<button class="tbtn ed-open-library" type="button"></button>
|
||||||
|
<p class="ed-library-hint"></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lg-ed-list"></div>
|
<div class="lg-ed-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -312,7 +316,7 @@ function mountLetterGame(root, initState, opts){
|
||||||
edThemeSel.innerHTML = "";
|
edThemeSel.innerHTML = "";
|
||||||
const og = document.createElement("option");
|
const og = document.createElement("option");
|
||||||
og.value = "g";
|
og.value = "g";
|
||||||
og.textContent = `${T("cardGeneral")} (${generalList().length})`;
|
og.textContent = `⭐ ${T("cardStart")} (${generalList().length})`;
|
||||||
edThemeSel.appendChild(og);
|
edThemeSel.appendChild(og);
|
||||||
/* thema's zonder map bovenaan, daarna per map gegroepeerd - zelfde
|
/* thema's zonder map bovenaan, daarna per map gegroepeerd - zelfde
|
||||||
indeling als fillWordSourceSel (data.js) in de widget-dropdowns */
|
indeling als fillWordSourceSel (data.js) in de widget-dropdowns */
|
||||||
|
|
@ -519,6 +523,7 @@ function mountLetterGame(root, initState, opts){
|
||||||
el(".ed-p-emoji").style.display = edTab==="emoji" ? "block" : "none";
|
el(".ed-p-emoji").style.display = edTab==="emoji" ? "block" : "none";
|
||||||
el(".ed-p-picto").style.display = edTab==="picto" ? "block" : "none";
|
el(".ed-p-picto").style.display = edTab==="picto" ? "block" : "none";
|
||||||
el(".ed-p-upload").style.display = edTab==="upload" ? "block" : "none";
|
el(".ed-p-upload").style.display = edTab==="upload" ? "block" : "none";
|
||||||
|
el(".ed-p-library").style.display = edTab==="library" ? "block" : "none";
|
||||||
if(edTab==="picto" && !edPSearch.value) edPSearch.value = edWord.value.trim();
|
if(edTab==="picto" && !edPSearch.value) edPSearch.value = edWord.value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -568,12 +573,25 @@ function mountLetterGame(root, initState, opts){
|
||||||
}
|
}
|
||||||
el(".ed-pgo").addEventListener("click", searchPictos);
|
el(".ed-pgo").addEventListener("click", searchPictos);
|
||||||
edPSearch.addEventListener("keydown", e=>{ if(e.key==="Enter") searchPictos(); });
|
edPSearch.addEventListener("keydown", e=>{ if(e.key==="Enter") searchPictos(); });
|
||||||
|
el(".ed-open-library").addEventListener("click", ()=>{
|
||||||
|
openImageCatalog((src,image)=>selectPic(image?.value||src));
|
||||||
|
});
|
||||||
/* typing a word auto-fills picto search */
|
/* typing a word auto-fills picto search */
|
||||||
edWord.addEventListener("input", ()=>{ edPSearch.value = edWord.value.trim(); });
|
edWord.addEventListener("input", ()=>{ edPSearch.value = edWord.value.trim(); });
|
||||||
|
|
||||||
/* upload tab */
|
/* upload tab */
|
||||||
edFile.addEventListener("change", ()=>{
|
edFile.addEventListener("change", async ()=>{
|
||||||
const f = edFile.files[0]; if(!f) return;
|
const f = edFile.files[0]; if(!f) return;
|
||||||
|
edFile.value = "";
|
||||||
|
if(currentUser){
|
||||||
|
const hint=el(".ed-uphint"),old=hint.textContent;
|
||||||
|
hint.textContent=T("imgUploading");
|
||||||
|
try{
|
||||||
|
const image=await uploadImageToCatalog(f,{name:edWord.value.trim(),folder:"Taal/Mijn woordkaarten"});
|
||||||
|
selectPic(image.src);
|
||||||
|
}catch(err){hint.textContent=err.message;setTimeout(()=>{hint.textContent=old},3500)}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = ()=>{
|
img.onload = ()=>{
|
||||||
const c = document.createElement("canvas");
|
const c = document.createElement("canvas");
|
||||||
|
|
@ -584,7 +602,6 @@ function mountLetterGame(root, initState, opts){
|
||||||
URL.revokeObjectURL(img.src);
|
URL.revokeObjectURL(img.src);
|
||||||
};
|
};
|
||||||
img.src = URL.createObjectURL(f);
|
img.src = URL.createObjectURL(f);
|
||||||
edFile.value = "";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
el(".ed-add").addEventListener("click", ()=>{
|
el(".ed-add").addEventListener("click", ()=>{
|
||||||
|
|
@ -615,6 +632,8 @@ function mountLetterGame(root, initState, opts){
|
||||||
el(".ed-title").textContent = T("manage");
|
el(".ed-title").textContent = T("manage");
|
||||||
el(".ed-uplbl").textContent = T("upload");
|
el(".ed-uplbl").textContent = T("upload");
|
||||||
el(".ed-uphint").textContent = T("upHint");
|
el(".ed-uphint").textContent = T("upHint");
|
||||||
|
el(".ed-open-library").textContent = "🗂 "+T("imgOpenLibrary");
|
||||||
|
el(".ed-library-hint").textContent = T("imgLanguageHint");
|
||||||
el(".ed-credit").innerHTML = T("pictoCredit");
|
el(".ed-credit").innerHTML = T("pictoCredit");
|
||||||
el(".ed-pgo").textContent = T("pictoSearch");
|
el(".ed-pgo").textContent = T("pictoSearch");
|
||||||
edPSearch.placeholder = T("pictoPh");
|
edPSearch.placeholder = T("pictoPh");
|
||||||
|
|
@ -757,4 +776,3 @@ function confetti(container){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
91
test/language-library.test.js
Normal file
91
test/language-library.test.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {readFile} from 'node:fs/promises';
|
||||||
|
import {runInNewContext} from 'node:vm';
|
||||||
|
|
||||||
|
async function languageData(extra=''){
|
||||||
|
const source=await readFile('public/js/data.js','utf8');
|
||||||
|
const context={LANG:'nl',T:key=>key};
|
||||||
|
runInNewContext(source+`; ${extra}
|
||||||
|
globalThis.__language={
|
||||||
|
concepts:LANGUAGE_CONCEPTS,
|
||||||
|
themesNl:languageThemeList("nl"),
|
||||||
|
themesEn:languageThemeList("en"),
|
||||||
|
words:WORDS,
|
||||||
|
catalogNl:languagePictureCatalog("nl")
|
||||||
|
};`,context);
|
||||||
|
return JSON.parse(JSON.stringify(context.__language));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('taalwidgets delen een ruime tweetalige woordenschat in acht duidelijke thema’s',async()=>{
|
||||||
|
const data=await languageData();
|
||||||
|
assert.ok(data.concepts.length>=140,'verwacht minimaal 140 vaste woordplaatjes');
|
||||||
|
assert.equal(data.themesNl.length,8);
|
||||||
|
assert.equal(data.themesEn.length,8);
|
||||||
|
assert.equal(new Set(data.concepts.map(item=>item.id)).size,data.concepts.length);
|
||||||
|
for(const concept of data.concepts){
|
||||||
|
assert.ok(concept.nl&&concept.en&&concept.pic,concept.id);
|
||||||
|
assert.ok(data.themesNl.some(theme=>theme.id===concept.theme),concept.theme);
|
||||||
|
}
|
||||||
|
for(const themes of [data.themesNl,data.themesEn]){
|
||||||
|
for(const theme of themes)assert.ok(theme.words.length>=10,theme.id);
|
||||||
|
assert.equal(themes.reduce((sum,theme)=>sum+theme.words.length,0),data.concepts.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('alle oude startwoorden blijven staan en krijgen ook een bibliotheekplaatje',async()=>{
|
||||||
|
const data=await languageData();
|
||||||
|
const oldNl=['aap','vis','zon','kat','bal','ei','boom','huis','maan','boot','kip','muis','boek','ster','roos','vuur','fiets','taart','jas','bril','klok','peer','kaas','eend','hand','voet','neus','oog','oor','mond'];
|
||||||
|
const oldEn=['cat','dog','sun','egg','ball','fish','tree','moon','boat','star','book','cake','house','mouse','apple','train','fire','rose','bear','duck','frog','bee','cow','pig','hand','foot','nose','eye','ear','milk'];
|
||||||
|
assert.deepEqual(data.words.nl.map(([word])=>word),oldNl);
|
||||||
|
assert.deepEqual(data.words.en.map(([word])=>word),oldEn);
|
||||||
|
const names=new Set(data.catalogNl.images.map(image=>image.name));
|
||||||
|
for(const word of oldNl)assert.ok(names.has(word),word);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('vaste en eigen woordplaatjes worden als herbruikbare catalogusitems aangeboden',async()=>{
|
||||||
|
const data=await languageData('GENERAL_EXTRA.nl=[["test","🧪"]]; THEMES.nl=[{name:"Boerderij",folder:"School",words:[["stal","🏚️"]]}];');
|
||||||
|
const builtins=data.catalogNl.images.filter(image=>image.builtin);
|
||||||
|
assert.equal(builtins.length,data.concepts.length);
|
||||||
|
assert.equal(data.catalogNl.themes.filter(theme=>theme.languageBuiltin).length,8);
|
||||||
|
for(const image of builtins){
|
||||||
|
assert.ok(image.id<0);
|
||||||
|
assert.ok(image.src.startsWith('data:image/svg+xml;charset=utf-8,'));
|
||||||
|
assert.ok(image.value&&image.themes.length===1);
|
||||||
|
assert.equal(image.canManage,false);
|
||||||
|
}
|
||||||
|
const testPic=data.catalogNl.images.find(image=>image.name==='test');
|
||||||
|
const stablePic=data.catalogNl.images.find(image=>image.name==='stal');
|
||||||
|
assert.equal(testPic.value,'🧪');
|
||||||
|
assert.equal(stablePic.value,'🏚️');
|
||||||
|
assert.equal(stablePic.folder,'Taal/Mijn thema’s/School/Boerderij');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('alle vier taalspellen gebruiken themabronnen en kunnen de bibliotheek openen',async()=>{
|
||||||
|
const [data,catalog,picker,letters,css,catalogCss]=await Promise.all([
|
||||||
|
readFile('public/js/data.js','utf8'),
|
||||||
|
readFile('public/js/image-catalog.js','utf8'),
|
||||||
|
readFile('public/js/picker.js','utf8'),
|
||||||
|
readFile('public/js/widgets/letters.js','utf8'),
|
||||||
|
readFile('public/css/teach.css','utf8'),
|
||||||
|
readFile('public/css/image-catalog.css','utf8'),
|
||||||
|
]);
|
||||||
|
assert.match(data,/card\.startsWith\("b:"\)/);
|
||||||
|
assert.match(data,/builtinGroup\.label = "📚 "/);
|
||||||
|
for(const widget of ['letters','flits','hak','ball']){
|
||||||
|
const source=await readFile(`public/js/widgets/${widget}.js`,'utf8');
|
||||||
|
assert.match(source,/wordSourceList\(/,widget);
|
||||||
|
assert.match(source,/fillWordSourceSel\(/,widget);
|
||||||
|
}
|
||||||
|
assert.match(catalog,/refreshWordPictureCatalog/);
|
||||||
|
assert.match(catalog,/languagePictureCatalog\(LANG\)/);
|
||||||
|
assert.match(catalog,/ic-word-themes/);
|
||||||
|
assert.match(picker,/data-t="library"/);
|
||||||
|
assert.match(picker,/uploadImageToCatalog\(f,/);
|
||||||
|
assert.match(letters,/data-t="library"/);
|
||||||
|
assert.match(letters,/openImageCatalog\(\(src,image\)/);
|
||||||
|
assert.match(letters,/uploadImageToCatalog\(f,/);
|
||||||
|
assert.match(css,/\.ed-tabs\{[^}]*overflow-x:auto/);
|
||||||
|
assert.match(catalogCss,/\.ic-word-themes\{[^}]*overflow-x:auto/);
|
||||||
|
assert.match(catalogCss,/@media\(max-width:760px\)/);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue